并发编程重点知识整理
一、怎么开进程(线程)
1. 开子进程(适合计算密集型程序)
1 from multiprocessing import Process 2 import os 3 4 5 def task(name): 6 print(f'子进程{name}开始干活', os.getpid()) 7 8 if __name__ == '__main__': 9 p = Process(target=task, args=('blbp',)) 10 p.start() 11 print('主进程', os.getpid())
2. 开子线程(适合I/O密集型程序)
1 from threading import Thread, currentThread 2 import time, random 3 4 5 def task(name): 6 time.sleep(random.randint(1, 3)) 7 print(f'子线程{name}', currentThread().getName()) 8 9 10 if __name__ == '__main__': 11 for i in range(10): 12 t = Thread(target=task, args=('blbp',)) 13 t.start() 14 print('主线程', currentThread().getName())
二、进程池(线程池)
1. 进程池
1 from concurrent.futures import ProcessPoolExecutor 2 import random, time 3 4 5 def extract(name): 6 l = [] 7 print(f'姓名{name}随机抽取一堆数字') 8 time.sleep(random.randint(1, 3)) 9 for i in range(10): 10 l.append(random.randint(1, 5)) 11 return name, l 12 13 14 def sum(data): 15 name, li = data.result() # 读取子进程的执行结果 16 time.sleep(random.randint(1, 5)) 17 res = 0 18 for n in li: 19 res += n 20 print(f"姓名{name}计算给出列表中的数字和{res}") 21 22 23 if __name__ == '__main__': 24 print('每个人随机抽取一堆数字,并计算出数字的和。') 25 pool = ProcessPoolExecutor(3) # 线程池为3,每次最多3人抽取数字 26 for i in range(10): # 模拟10个人 27 pool.submit(extract, i).add_done_callback(sum) # 异步提交+回调函数 28 # pool.shutdown() # 等待子进程执行完成 29 print('主进程')
2. 线程池(用法与进程池一样,模块换一下即可)
1 from concurrent.futures import ThreadPoolExecutor
三、join
1. Process对象的join方法
1.1 什么情境下使用join
情况一:主进程比子进程先执行完毕,但主进程还需等待子进程结束,然后统一回收资源;
情况二:主进程需要在子进程执行完毕后继续执行。
1.2 对象join方法实例
1 def task(name): 2 print(f'子进程{name}开始干活', os.getpid()) 3 4 if __name__ == '__main__': 5 p = Process(target=task, args=('blbp',)) 6 p.start() 7 p.join() # 等待p执行完成,才执行下一行代码。 8 print('主进程', os.getpid())
特别说明:程序中有了p.join(),绝对不是把程序由并发变成串行,如果有多个P子进程、每个p.join,所有子进程仍然会并发执行,只有主进程会等待。
2. Thread对象的join方法(参考Process,用法一致)
四、daemon守护进程(守护线程)
1. 守护进程
1.1 守护进程强调两点:
其一:守护进程会在主进程代码执行结束后就终止(即使守护进程还未执行完也要终止),崇祯皇帝一死,守护他的老太监也跟着殉葬。
其二:守护进程内无法再开启子进程。
1.2 守护进程使用情景:
如果我们有两个任务需要并发执行,那么开一个主进程和一个子进程分别去执行就OK了,如果子进程的任务在主进程任务结束后就没有存在的必要了,那么该子进程应该就被设置为守护进程。主进程代码运行结束,守护进程随即终止。
1 from multiprocessing import Process 2 import os 3 4 5 def task(name): 6 print(f'子进程{name}开始干活', os.getpid()) 7 8 if __name__ == '__main__': 9 p = Process(target=task, args=('blbp',)) 10 p.daemon = True # 一定要在p.start()前设置,设置p为守护进程,禁止p创建子进程,并且父进程代码执行结束,p随即终止运行。 11 p.start() 12 print('主进程', os.getpid())
2.守护线程
2.1 守护线程与守护进程的相同点
他们都遵循:守护XXX会等待主XXX运行完毕后被销毁
2.2 守护线程与守护进程的不同点
需要强调的是:运行完毕并非终止运行,主进程主线程运行完毕的意思是不一样的。
对比一:对主进程来说,运行完毕指的是主进程代码运行完毕;
对比二:对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕。
详细解释:
其一、主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束。
其二、主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就会被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
3.守护线程守护进程实例对比
3.1 守护进程
1 from multiprocessing import Process 2 import time 3 4 5 def foo(): 6 print(123) 7 time.sleep(1) 8 print("end123") 9 10 11 def bar(): 12 print(456) 13 time.sleep(3) 14 print("end456") 15 16 17 if __name__ == '__main__': 18 p1 = Process(target=foo) 19 p2 = Process(target=bar) 20 21 p1.daemon = True 22 p1.start() 23 p2.start() 24 print("main-------")
运行结果:
main-------
456
end456
为什么p1未能打印出结果?因为它是守护进程,主进程一完成,它还未来得及打印就跟着结束了。
为什么主进程最先打印出,因为操作系统开启一个新的进程开辟内存空间开销大,需要时间。
为什么p2进程从头到尾顺利完成?因为进程之间是互相隔离的,各干各的,除非是守护进程.
3.2 守护线程
1 from threading import Thread 2 import time 3 4 def sayhi(name): 5 time.sleep(2) 6 print('%s say hello' %name) 7 8 if __name__ == '__main__': 9 t = Thread(target=sayhi, args=('xiaojun', )) 10 # t.setDaemon(True) # 守护线程的两种写法 11 t.daemon = True 12 t.start() 13 14 print('主线程') 15 print(t.is_alive()) # 验证子线程t是否存活,此行的pint()是主线程,其参数是zi线程。
输出结果:
主线程
True
为什么输出了True?因为15行print()本身是主线程输出,所以此时子线程任然存活。但当print()后主线程结束,子线程也死了,子线程没能输出say hello。
1 from threading import Thread 2 import time 3 4 5 def foo(): 6 print(123) 7 time.sleep(1) 8 print("end123") 9 10 11 def bar(): 12 print(456) 13 time.sleep(3) 14 print("end456") 15 16 17 if __name__ == '__main__': 18 t1 = Thread(target=foo) 19 t2 = Thread(target=bar) 20 21 t1.daemon = True 22 t1.start() 23 t2.start() 24 print("main-------")
运行结果:
123 456 main------- end123 end456
为什么t1能打印出结果?主要原因是有非守护线程t2的存在,主线程必须要等到非守护线程运行完成(这个特点与主进程是不同的)。
如果没有t2,则t1只能打印出123,后面的end123无法打印,因为主线程结束了,守护线程也跟着结束。
开启新线程,系统开销极小,所以不耗费时间。
五、进程队列、线程队列
1. 进程队列作用:
进程彼此间是隔离的,队列用于进程间的通信(IPC),队列底层就是以管道+锁的方式实现的。
2.进程Queue队列类的解释:
Queue([maxsize]),maxsize是队列中允许最大项数,省略则无大小限制。
注意一、队列内存放的是消息而非大数据;注意二、队列占用的是内存空间,因而maxsize即便是无大小限制也受限于内存大小。
3.进程Queue主要用法介绍:
q.put方法用以插入数据到队列中、q.get方法可以从队列读取并且删除一个元素。 q.full()是否满了,q.empty()是否空了。
4.进程队列的使用范例:
1 from multiprocessing import Process, Queue 2 import time, random 3 4 5 def task(name, q): 6 print(f'子进程{name}在工作') 7 time.sleep(random.randint(1, 3)) 8 q.get() # 3. 一个子线程执行完毕,从队列中取出一个元素。原本阻塞的队列,for循环可以再放入1个元素。 9 10 11 if __name__ == '__main__': 12 q = Queue(2) # 1. 创建队列。 队列设置为2,表示同时只能有两个进程在工作 13 14 for i in range(10): 15 q.put(1) # 2. 把一个元素放入队列中,队列中元素达到2个时,再put将阻塞。 16 p = Process(target=task, args=(i, q)) 17 p.start()
使用时需要再注意的是:队列满了,再尝试放入会阻塞、队列被取空后再尝试取也会阻塞。
5.线程queue:基本用法同进程队列,导入模块不同,线程队列有三个不同类。
1. 线程queue导入模块:import queue
2. q = queue.Queue(maxsize) # 队列:先进先出
1 import queue 2 3 q=queue.Queue() 4 q.put('first') 5 q.put('second') 6 q.put('third') 7 8 print(q.get()) 9 print(q.get()) 10 print(q.get())
''' 结果(先进先出): first second third '''
3. q = queue.LifoQueue(maxsize) # 堆栈:后进先出
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 '''
4. q = queue.PriorQueue(maxsize) # 优先级队列:存储数据时可设置优先级的队列
1 import queue 2 3 q=queue.PriorityQueue() 4 #put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高 5 q.put((20,'a')) 6 q.put((10,'b')) 7 q.put((30,'c')) 8 9 print(q.get()) 10 print(q.get()) 11 print(q.get())
''' 结果(数字越小优先级越高,优先级高的优先出队): (10, 'b') (20, 'a') (30, 'c') '''
六、生产者、消费者模型
1. 模型解释
1.1 程序中有两类角色
一类负责生产数据(生产者)
一类负责处理数据(消费者)
1.2 引入生产者消费者模型为了解决的问题是
目的一、平衡生产者与消费者之间的速度查
目的二、程序解开耦合
1.3 如何实现生产者消费者模型
生产者<--->队列<--->消费者
1.4 为什么要使用生产者消费者模型
生产者指的是生产数据的任务,消费者指的是处理数据的任务,在并发编程中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
1.5 什么是生产者和消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
这个阻塞队列就是用来给生产者和消费者解耦的
2. 模型实例
1 from multiprocessing import Process, Queue 2 import time, random 3 4 5 6 def consumer(q): 7 # 消费者(处理数据) 8 while True: # 消费者不停地消费 9 food = q.get() # 消费者从队列中获取数据。 10 if food is None: break # 消费者接收到结束信号 11 time.sleep(random.randint(1, 3)) 12 print(f'消费者吃了一个{food}') # 处理数据 13 14 15 def producer(q): 16 # 生产者(生产数据) 17 for i in range(3): 18 time.sleep(random.randint(1, 2)) 19 food = f'食物{i}' 20 print(f'生产者成产了{food}') # 生产数据 21 q.put(food) # 生产者向队列中放数据 22 23 24 if __name__ == '__main__': 25 q = Queue() # 建立队列 26 27 # 生产者 28 p = Process(target=producer, args=(q,)) 29 # 消费者 30 c = Process(target=consumer, args=(q,)) 31 32 p.start() # 向操作系统提交生成进程的请求 33 c.start() 34 35 p.join() # 等待生产者成产完成 36 q.put(None) # 向消费者发送结束信号(有几个消费者就要发几个None),防止队列为空时消费者get()时阻塞。 37 print('主进程')
七、互斥锁、死锁、递归锁、GIL
1.为什么要用互斥锁?
互斥锁的意思就是相互排斥。比如:多个人要去争抢同一个资源-卫生间,一个人抢到卫生间后上一把锁,其他人都要等着,等到这个完成任务后释放锁,其他人才有一个抢到。。。所以互斥锁的原理,就是把并发改成串行,降低了效率,但保证了数据安全不错乱。
2.互斥锁使用方法
1 # 由并发编程了串行,牺牲了运行效率,但避免了竞争 2 def work(lock): 3 lock.acquire() # 加锁 4 print('%s is running' % os.getpid()) 5 time.sleep(2) 6 print('%s is done' % os.getpid()) 7 lock.release() # 释放锁 8 9 if __name__ == '__main__': 10 lock = Lock() 11 for i in range(3): 12 p = Process(target=work, args=(lock,)) 13 p.start()
3.死锁
是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
4.递归锁
递归锁就是用来解决死锁的。在python中为了支持在同一线程中多次请求同一资源,python提供了可重如锁RLock.
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require.直到一个线程所有的acquire都被release,其它线程才能获得资源.
递归锁与互斥锁的区别:递归锁可以连续多次acquire,而互斥锁只能acquire一次.
5.python GIL
GIL本质是一把互斥锁,它是cpython解释器级别的互斥锁,保护的是解释器级别的数据.(比如垃圾回收)
本质上:python程序一个进程中,同时只能运行一个线程,保护不同的数据应该加不同的锁,解释器级别的数据就由GIL控制.
用户自己开发的应用程序数据由用户自行加锁Lock处理.
八、协程
1.什么是协程?
协程是但线程下的并发.协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的.本质是一个线程.
注意:
一.python的线程属于内核级别的,即由操作系统控制调度.
二.单线程内开启协程,一旦遇到IO,就会从应用程序级别(而非操作系统)控制切换,以此来提示效率.(非IO操作的切换与效率无关)
2.与线程的对比
优点:开销更小,更加轻量级.最大限度利用CPU
缺点:本质是单线程,无法利用多核.协程是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
3.协程特点总结:
其一:必须只有一个单线程里实现并发
其二:修改共享数据不需加锁
其三:用户程序自己保存多个控制流的上下栈
其四:一个协程遇到IP操作自动切换到其它协程(如何实现检测IO,yidld,greenlet都无法实现,就用到gevent模块(select机制))
4.gevent模块
pip install gevent # 安装第三方模块gevent
gevent模块可以自动帮我们识别程序中的IO阻塞,并自动切换任务.
如果要识别time等其它模块产生的阻塞,需要加补丁
from gevent import monkey;monkey.patch_all() # 加补丁,需要放在其它导入库的前面
from gevent
5.使用gevent模块实现协程
1 from gevent import monkey; 2 3 monkey.patch_all() 4 5 import gevent 6 import time 7 8 9 def eat(name): 10 print('%s eat 1' % name) 11 time.sleep(3) 12 print('%s eat 2' % name) 13 14 15 def play(name): 16 print('%s play 1' % name) 17 time.sleep(4) 18 print('%s play 2' % name) 19 20 21 if __name__ == '__main__': 22 g1 = gevent.spawn(eat, 'egon') 23 g2 = gevent.spawn(eat, 'alex') 24 25 # g1.join() 26 # g2.join() 27 gevent.joinall([g1, g2]) # 等待g1,g2运行完成
1 # 多协程并发通信服务端 2 from gevent import monkey; 3 4 monkey.patch_all() 5 from socket import * 6 import gevent 7 8 9 def server(server_ip, port): 10 s = socket(AF_INET, SOCK_STREAM) 11 s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) 12 s.bind((server_ip, port)) 13 s.listen(5) 14 while True: 15 conn, addr = s.accept() 16 gevent.spawn(talk, conn, addr) 17 18 19 def talk(conn, addr): 20 try: 21 while True: 22 res = conn.recv(1024) 23 print('client %s:%s msg: %s' % (addr[0], addr[1], res)) 24 conn.send(res.upper()) 25 except Exception as e: 26 print(e) 27 finally: 28 conn.close() 29 30 31 if __name__ == '__main__': 32 server('127.0.0.1', 8080)
九、IO模型(协程gevent模块实现的原理)
1.为什么要解决单线程下的IO问题?
开进程,开线程能够提升效率,但是受计算机性能的限制,可开的进程数、线程数是有限的。
使用进程池、线程池是可以防止开过多的进程、线程导致计算机崩溃,但显然它只适合小规模的访问需求。
2.IO模型的类型
2.1.阻塞IO,遇到IO,不做任何处理,就原地等待(最早学习做的socket网络通信就是阻塞IO),实际意义不大。
2.2.非阻塞IO,反复询问操作系统,遇到IO,就切到程序中的其它部分,过一定时间再询问是否阻塞。CPU会一直停留在该程序,该程序率高,CPU占用高,但存在空转等现象,影响其它程序运行.不推荐使用非阻塞IO.
2.3.多路复用IO,有中介的阻塞IO,采用select模块,中介代理多个IO(检测多个套接字的IO),轮询操作系统.
2.4.异步IO,效率最高,请求发出后,就不管了.