一、协程
-
协程,又称微线程
-
协程是python中另外一种实现多任务的方式,只不过比线程更小,占用更小执行单元(理解为需要的资源)
-
它自带CPU上下文,这样只要在合适的时间,我们就可以把一个协程切换到另一个协程,只要这个过程保存或恢复CPU上下文那么程序还是可以运行的
通俗的理解
在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己决定
协程和线程差异
在实现多任务时,线程切换从系统层面远不止保存和恢复CPU上下文这么简单,操作系统为了程序的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。所以线程的切换比较耗性能。但是协程的切换之间单纯的操作CPU的上下文,所以一秒钟切换个上百万次性能都扛得住。
# 协程的实现: 通过含yield关键字的生成器函数来实现协程 def work1(): for i in range(10): print('---work1获取数据{}'.format(i)) yield i def work2(): for i in range(10) : print('---work2获取数据{}'.format(i)) yield i def main(): g1=work1() g2=work2() while True: try: next(g1) next(g2) except StopIteration: pass if __name__ == '__main__': main()
为了更好地使用协程,greenlet模块对其进行了封装,使得任务切换变得更简单
greenlet $ pip install greenlet import time from greenlet import greenlet def test1(): for i in range(5): print(i) g2.switch() time.sleep(0.1) def test2(): for i in range(5): print(i) g1.switch() time.sleep(0.1) g1 = greenlet(test1) g2 = greenlet(test2) g1.switch()
二、gevent:比greenlet更便捷,省去人工切换,遇到IO操作时可以自动切换协程
gevent其原理就是当一个greenlet遇到IO(input output输入输出,比如网络,文件操作等)操作时,比如访问网络,就自动切换到其他greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent自动切换协程,保证总有greenlet在运行,而不是等待IO
# pip install gevent 等待需要使用gevent.sleep(0.1) import gevent def work1(): for i in range(5): print(f'work1:{i}') gevent.sleep(0.1) # 遇到gevent.sleep就会切换到其他可运行的线程
def work2(): for i in range(5): print(f'work2:{i}') gevent.sleep(0.1) g1 = gevent.spawn(work1) g2 = gevent.spawn(work2) g1.join() # 设置主线程 等待子协程执行完之后 往下执行 g2.join()
如果需要使用time.sleep
但是要完成切换需要使用猴子补丁
注意:猴子补丁在多线程任务时会出现异常!!!!!不建议使用。
from gevent import monkey import time import gevent from gevent import monkey monkey.patch_all() def work1(): for i in range(5): print(f'work1:{i}') time.sleep(0.1) def work2(): for i in range(5): print(f'work2:{i}') time.sleep(0.4) g1 = gevent.spawn(work1) g2 = gevent.spawn(work2) g1.join() g2.join()
发送IO
import time import requests import gevent def usetime(func): def wrapper(*args, **kwargs): start_time = time.time() func(*args, **kwargs) end_time = time.time() print(f'耗时{end_time - start_time}') return wrapper def work1(url): for i in range(100): requests.get(url) gevent.sleep(0.01) print(f'work1发送第{i}次请求') def work2(url): for i in range(100): requests.get(url) gevent.sleep(0.01) print(f'work2发送第{i}次请求') @usetime def main(): # def spawn(cls, *args, **kwargs): g1 = gevent.spawn(work1, url='https://www.baidu.com') g2 = gevent.spawn(work2, 'https://www.baidu.com') g1.join() g2.join() if __name__ == '__main__': main() # 耗时32.37710213661194
三、进程池
-
当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process成生多个进程,但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,此时就可以用到multiprocessing模块提供的Pool方法。
初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会用之前的进程来执行新的任务
-
multiprocessing.Pool常用函数解析:
apply_async(func[, args[, kwds]]) :使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表;
close():关闭Pool,使该进程池不再接受新的任务;
terminate():不管任务是否完成,立即终止;
join():主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用;
import os import time from multiprocessing import Pool def work1(): for i in range(5): print(f'进程ID:{os.getpid()},work1:{i}') time.sleep(0.1) if __name__ == '__main__': # 创建一个进程池,进程池中有五个进程 po = Pool(5) for i in range(3): po.apply_async(work1) po.close() # 进程池不再接收任务 po.join()
进程池之间的队列:进程池中的进程如何通信
from multiprocessing import Manager,Pool # 修改import中的Queue为Manager import os,time,random def reader(q): for i in range(q.qsize()): print("reader从Queue获取到消息:%s" % q.get(True)) def writer(q): for i in "helloword": q.put(i) if __name__=="__main__": q = Manager().Queue() # 进程池创建Queue队列 po = Pool() po.apply_async(writer, (q,)) time.sleep(1) # 先让上面的任务向Queue存入数据,然后再让下面的任务开始从中取数据 po.apply_async(reader, (q,)) po.close() po.join() print("(%s) End" % os.getpid())
四、线程池
-
内置模块:from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
-
第三方模块:import threadpool
五、进程线程协程比较
-
进程是资源分配的单位
-
线程是操作系统调度的单位
-
进程切换需要的资源最大,效率很低
-
线程切换需要的资源一般,效率一般(在不考虑GIL的情况下)
-
协程切换任务资源很小,效率高
-
多进程,多线程根据cpu核数不同可能是并行的,但是协程是在一个线程中的,所以是并发
-
python中的线程由于GIL锁的存在,不能实现并行