并发编程【五】线程
线程
互斥锁:在同一个进程内,也有锁的竞争关系
在同一个进程中连续acquire多次会产生死锁
from multiprocessing import Lock """ 互斥锁:在同一个进程内,也有锁的竞争关系 在同一个进程中连续acquire多次会产生死锁 """ lock = Lock() lock.acquire() # 拿走钥匙 print(123) lock.acquire() # 又想拿钥匙 卡住 print(456) lock = Lock() lock.acquire() # 拿走钥匙 print(123) lock.release() # 还钥匙 lock.acquire() # 第二个人才能拿到钥匙,继续操作 print(456)
Queue队列
如何解决Queue中,往队列里放的值超出设定值的阻塞问题,及put_nowait数据丢失问题
import queue from multiprocessing import Queue q = Queue(5) q.put(1) q.put(1) q.put(1) q.put(1) q.put(1) print('阻塞前') try: q.put_nowait(1) # 放不进去 了,数据丢失 except queue.Full: # 想要数据不丢失,自己创建个容器,在except中将数据添加到容器中 pass print('阻塞后') print(q.get()) print(q.get()) print(q.get()) print(q.get()) print(q.get()) try: q.get_nowait() # 没有值的话就不会在等了 except queue.Empty: pass
进程和线程的关系
线程的创建
线程的创建方式和进程的创建方式大致相同
创建线程用threading模块
Thread实例对象的方法 # isAlive(): 返回线程是否活动的。 # getName(): 返回线程名。 # setName(): 设置线程名。 threading模块提供的一些方法: # threading.currentThread(): 返回当前的线程变量。 # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
import os import time from threading import Thread # 创建线程 def func(i): time.sleep(0.2) print('in func', i ,os.getpid()) print('in main',os.getpid()) for i in range(20): Thread(target=func,args=(i,)).start() """ 开的20个线程是在这个进程内的所以他们的进程id就是本程序的进程id if __name__ == '__main__'在开启线程的时候可以不加 在线程部分不需要通过import来为新的线程获取代码 因为新的线程和之前的主线程共享同一段代码 不需要import 也就不存在在子线程中又重复了一次创建线程的操作 所以就不必要if __name__ == '__main__' """ 线程的创建
from threading import Thread def func(): print('这里是线程') Thread(target=func).start() # 创建一个线程
from threading import Thread def func(): print('这里是线程') for i in range(100): Thread(target=func).start() # 创建一个线程
from threading import Thread from multiprocessing import Process import os def work(): print('hello',os.getpid()) if __name__ == '__main__': #part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样 t1=Thread(target=work) t2=Thread(target=work) t1.start() t2.start() print('主线程/主进程pid',os.getpid()) #part2:开多个进程,每个进程都有不同的pid p1=Process(target=work) p2=Process(target=work) p1.start() p2.start() print('主线程/主进程pid',os.getpid()) 多线程与多进程pid比较
开销 开启进程\线程与销毁进程\线程 import time from multiprocessing import Process from threading import Thread def func(a): a += 1 if __name__ == '__main__': start = time.time() t_lis = [] for i in range(100): t = Thread(target=func, args=(i,)) t.start() t_lis.append(t) for j in t_lis: j.join() print('threading', time.time() - start) # 计算线程的开启与销毁时间threading 0.01236867904663086 p_lis = [] for i in range(100): p = Process(target=func,args=(i,)) p.start() p_lis.append(p) for j in p_lis:j.join() print('process:',time.time()-start) # 计算进程的开启与销毁时间process: 1.4895079135894775 进程和线程的开启效率
多个线程之间的全局变量是共享的 from threading import Thread pn = 0 def func(): global pn pn+=1 t_l = [] for i in range(100): t = Thread(target=func) # 每个线程都去加1,数据共享 t.start() t_l.append(t) for j in t_l:j.join() print(pn) # 100 进程之间数据隔离 from multiprocessing import Process pn = 0 def func(): global pn pn+=1 if __name__ == '__main__': t_l = [] for i in range(100): t = Process(target=func) # 根本操作不到子进程 t.start() t_l.append(t) for j in t_l:j.join() print(pn) # 0 线程与进程内存数据的共享问题
import time import random from threading import Thread,currentThread,active_count def func(): time.sleep(random.random()) print(currentThread().name) for i in range(10): Thread(target=func).start() print(active_count()) # 返回当前有多少个正在工作的线程 print(currentThread().name) # 查看当前线程的变量 for i in range(10): t = Thread(target=func) t.start() print(t) # <Thread(Thread-1, started 11544)> print(currentThread()) # <Thread(Thread-1, started 11544)> 他俩是一样的效果 threading的其他用法
import time import random from threading import Thread,currentThread,active_count def func(): time.sleep(random.random()) print(currentThread().name) for i in range(10): Thread(target=func).start() print(active_count()) # 返回当前有多少个正在工作的线程 print(currentThread().name) # 查看当前线程的变量 for i in range(10): t = Thread(target=func) t.start() print(t) # <Thread(Thread-1, started 11544)> print(currentThread()) # <Thread(Thread-1, started 11544)> 他俩是一样的效果 threading的其他用法
按照顺序把列表中的每一个元素都计算平方,使用多线程方式,并且结果按照顺序返回
import time import random from threading import Thread,currentThread dic = {} # {15020: 64, 11284: 25, 8488: 100, 4464: 9, 11488: 36, 14152: 4, 12068: 16, 8080: 81, 8496: 1, 5788: 49} def func(a): p = a * a time.sleep(random.random()) t = currentThread() dic[t.ident]= p # {15020: 64} lst = [] # [<Thread(Thread-1, stopped 8496)>, <Thread(Thread-2, stopped 14152)>, <Thread(Thread-3, stopped 4464)>, for i in range(1,11): t = Thread(target=func,args=(i,)) t.start() lst.append(t) # 将线程对象添加到列表中 t.ident查看id for j in lst: j.join() print(dic[j.ident]) # 取值的时候根据你添加时候的线程id取值 from threading import Thread def func(): print('这里是线程') for i in range(100): Thread(target=func).start() # 创建一个线程 按顺序返回结果
发布了任务不代表立即执行,执行任务需要操作系统帮我们调度,因为时间片轮转什么时候操作系统不忙了或者轮到某个线程了才会执行相应的任务;
全局解释器锁GIL
Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
在多线程环境中,Python 虚拟机按以下方式执行:
a、设置 GIL;
b、切换到一个线程去运行;
c、运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));
d、把线程设置为睡眠状态;
e、解锁 GIL;
d、再次重复以上所有步骤。
在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL。
消除GIL副作用的两种方案:
1.使用多进程,让多个核心都工作
2.使用其他语言编写的解释器,此时使用多线程,也能运行在多个核心上.
守护线程
无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。需要强调的是:运行完毕并非终止运行
#1.对主进程来说,运行完毕指的是主进程代码运行完毕 #2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
#1 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束, #2 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
import time from threading import Thread def daemon_func(): # 主进程会等待子线程的结束而结束 while True: time.sleep(0.2) print('子线程') Thread(target=daemon_func).start() 主进程会等待子线程的结束而结束
import time from threading import Thread def daemon_func(): # 守护线程会等主线程结束而结束 while True: time.sleep(1) print('守护线程') t = Thread(target=daemon_func) t.daemon = True t.start() time.sleep(5) print('主线程结束') 守护线程会等主线程结束而结束
mport time from threading import Thread def daemon_func(): while True: time.sleep(1) print('守护线程') def son_func(): print('start son') time.sleep(1) print('end son') t = Thread(target=daemon_func) t.daemon = True t.start() Thread(target=son_func).start() time.sleep(5) print('主线程结束') 守护线程会守护主线程和所有的子线程
进程会随着主线程的结束而结束
问题: 1.主线程需不需要回收子线程的资源 不需要,线程资源属于进程,所以进程结束了,线程的资源自然就被回收了 2.主线程为什么要等待子线程结束之后才结束 主线程结束意味着进程结束,进程结束,所有的子线程都会结束 要想让子线程能够顺利执行完,主线程只能等 3.守护线程到底是怎么结束的 主线程结束了,主进程也结束,守护线程被主进程的结束结束掉了 守护进程 :只会守护到主进程的代码结束 守护线程 :会守护所有其他非守护线程的结束
GIL和锁的关系
# 数据不安全问题 # 在线程中也是会出现数据不安全的 # 1.对全局变量进行修改 # 2.对某个值 += -= *= /= list[0] += 1 dic[key] -= 1 # 通过加锁来解决
# list pop append extend insert remove 安全
# dict pop update 安全
# list pop/append pop列表为空的时候会报错
# queue put/get get队列为空的时候会等待
import time from threading import Thread,Lock count = 0 def add_func(): global count for i in range(100000): count += 1 def sub_func(): global count time.sleep(0.2) for i in range(100000): count -= 1 lis = [] for i in range(5): t = Thread(target=add_func) # 开5个线程做10万加一操作 t.start() lis.append(t) t1 = Thread(target=sub_func) # 开5个线程做10万减一操作 t1.start() lis.append(t1) for j in lis:j.join() print(count) 数据不安全
import time from threading import Thread,Lock count = 0 def add_func(lock): global count for i in range(100000): with lock: count += 1 def sub_func(lock): global count time.sleep(0.1) for i in range(100000): with lock: count -= 1 lis = [] lock = Lock() for i in range(5): t = Thread(target=add_func,args=(lock,)) # 开5个线程做10万加一操作 t.start() lis.append(t) t1 = Thread(target=sub_func,args=(lock,)) # 开5个线程做10万减一操作 t1.start() lis.append(t1) for j in lis:j.join() print(count) 加锁后计算
为什么有了GIL锁还要给线程加锁?
因为GIL锁只能保证原子型操作的数据安全(append),对于那些可拆分(a+=1)的来说修改数据还是不安全;所以需要我们自己加锁来保证数据安全:
递归锁
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁
from threading import Lock as Lock import time mutexA=Lock() mutexA.acquire() mutexA.acquire() print(123) mutexA.release() mutexA.release() 死锁
解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:
from threading import RLock as Lock import time mutexA=Lock() mutexA.acquire() mutexA.acquire() print(123) mutexA.release() mutexA.release() 递归锁
典型问题:科学家吃面问题
import time from threading import Thread,Lock noodle_lock = Lock() fork_lock = Lock() def eat1(name): noodle_lock.acquire() print('%s 抢到了面条'%name) fork_lock.acquire() print('%s 抢到了叉子'%name) print('%s 吃面'%name) fork_lock.release() noodle_lock.release() def eat2(name): fork_lock.acquire() print('%s 抢到了叉子' % name) time.sleep(1) noodle_lock.acquire() print('%s 抢到了面条' % name) print('%s 吃面' % name) noodle_lock.release() fork_lock.release() for name in ['哪吒','egon','yuan']: t1 = Thread(target=eat1,args=(name,)) t2 = Thread(target=eat2,args=(name,)) t1.start() t2.start() 死锁
import time from threading import Thread,RLock fork_lock = noodle_lock = RLock() def eat1(name): noodle_lock.acquire() print('%s 抢到了面条'%name) fork_lock.acquire() print('%s 抢到了叉子'%name) print('%s 吃面'%name) fork_lock.release() noodle_lock.release() def eat2(name): fork_lock.acquire() print('%s 抢到了叉子' % name) time.sleep(1) noodle_lock.acquire() print('%s 抢到了面条' % name) print('%s 吃面' % name) noodle_lock.release() fork_lock.release() for name in ['哪吒','egon','yuan']: t1 = Thread(target=eat1,args=(name,)) t2 = Thread(target=eat2,args=(name,)) t1.start() t2.start() 递归锁解决死锁问题 递归锁解决死锁问题
线程队列
queue队列 :使用import queue,用法与进程Queue一样
class queue.
Queue
(maxsize=0) #先进先出
import queue q=queue.Queue() q.put('first') q.put('second') q.put('third') print(q.get()) print(q.get()) print(q.get()) ''' 结果(先进先出): first second third ''' 先进先出
class queue.
LifoQueue
(maxsize=0) #last in fisrt ou
import queue q=queue.LifoQueue() q.put('first') q.put('second') q.put('third') print(q.get()) print(q.get()) print(q.get()) ''' 结果(后进先出): third second first ''' 后进先出
class queue.
PriorityQueue
(maxsize=0) #存储数据时可设置优先级的队列
import queue q=queue.PriorityQueue() #put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高 q.put((20,'a')) q.put((10,'b')) q.put((30,'c')) print(q.get()) print(q.get()) print(q.get()) ''' 结果(数字越小优先级越高,优先级高的优先出队): (10, 'b') (20, 'a') (30, 'c') ''' 优先级队列
进程/线程池
#1 介绍 concurrent.futures模块提供了高度封装的异步调用接口 ThreadPoolExecutor:线程池,提供异步调用 ProcessPoolExecutor: 进程池,提供异步调用 Both implement the same interface, which is defined by the abstract Executor class. #2 基本方法 #submit(fn, *args, **kwargs) 异步提交任务 #map(func, *iterables, timeout=None, chunksize=1) 取代for循环submit的操作 #shutdown(wait=True) 相当于进程池的pool.close()+pool.join()操作 wait=True,等待池内所有任务执行完毕回收完资源后才继续 wait=False,立即返回,并不会等待池内的任务执行完毕 但不管wait参数为何值,整个程序都会等到所有任务执行完毕 submit和map必须在shutdown之前 #result(timeout=None) 取得结果 #add_done_callback(fn) 回调函数 # done() 判断某一个线程是否完成 # cancle() 取消某个任务 介绍
rom concurrent.futures import ProcessPoolExecutor def get_html(i): return i**2 def parser_page(n): print('进程将值传给了我%s'%n.result()) # 10个任务异步执行,谁先执行完就将结果传给我 if __name__ == '__main__': p = ProcessPoolExecutor(5) # 进程池中放几个进程,得到进程池对象 ret = p.map(get_html,range(10)) # 绑定方法,提交任务,返回一个可迭代对象(生成器) for i in ret: print(i) # ------------------------------------------------------- lis = [] for i in range(10): ret = p.submit(get_html, i) # 绑定方法,提交任务,接收到任务对象 lis.append(ret) p.shutdown() for j in lis: print(j.result()) # 为了防止阻塞我们将所有的任务对象都添加到列表里 # ------------------------------------------------------- for i in range(10): ret = p.submit(get_html,i) # 绑定方法,提交任务,接收到任务对象 print(ret.result()) # 获取任务函数的返回值,(阻塞方法) ret.add_done_callback(parser_page) # 回调函数,每执行完一个任务都执行一次回调函数(ret传给parser_page) p.shutdown() # 等待进程池中所有的进程都结束完毕结束阻塞(阻塞方法) 创建一个进程池
ret = p.map(get_html,range(10)) # 绑定方法,提交任务,返回一个可迭代对象(生成器) # 池对象的方法: submit() # 绑定方法,提交任务,接收到任务对象 shutdown() # 等待进程池中所有的进程都结束完毕结束阻塞(阻塞方法) # 任务对象的方法: result() # 获取任务函数的返回值,(阻塞方法) add_done_callback() # 回调函数,每执行完一个任务都执行一次回调函数(都将任务结果传给回调函数)
from urllib.request import urlopen from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor def get_html(name,addr): ret = urlopen(addr) return {'name':name,'content':ret.read()} def parser_page(ret_obj): dic = ret_obj.result() with open(dic['name']+'.html','wb') as f: f.write(dic['content']) url_lst = { '协程':'http://www.cnblogs.com/Eva-J/articles/8324673.html', '线程':'http://www.cnblogs.com/Eva-J/articles/8306047.html', '目录':'https://www.cnblogs.com/Eva-J/p/7277026.html', '百度':'http://www.baidu.com', 'sogou':'http://www.sogou.com' } t = ThreadPoolExecutor(20) for url in url_lst: task = t.submit(get_html,url,url_lst[url]) # 使用多线程去执行get_html获取网页对应的内容 # 一旦get_html执行结束之后,立即使用parser_page来分析获取的页面结果 task.add_done_callback(parser_page) # 进程池 除非高计算型的场景否则几乎不用 CPU的个数*2 # 线程池 cpu的个数*5 # 4 cpu 4*20 = 80 创建一个线程池
线程实现socket server并发
import socket from threading import Thread sk = socket.socket() sk.bind(('127.0.0.1',9000)) sk.listen() def func(conn): while True: msg = conn.recv(1024).decode('utf-8') print(msg) conn.send(b'hello') while True: conn,addr = sk.accept() t = Thread(target=func,args=(conn,)) t.start() server