大家好,我是小梁,今天给大家分享关于操作系统中程序运行很重要的几个概念,而我把它们称为三剑客:进程、线程、协程。
今天主讲协程。
关注我,每天让您获取来自编程的乐趣。
1、概念
1.1 协程
协程:比线程更小的执行单元,又称微线程,在单线程上执行多个任务,自带CPU上下文。用函数切换,开销极小。不通过操作系统调度,没有进程、线程的切换开销。
那么线程与协程有什么区别呢?
我们假设把一个进程比作我们实际生活中的一个拉面馆,负责保持拉面馆运行的服务员就是线程,每个餐桌点菜代表要完成的任务。
当我们用多线程完成任务时,模式是这样的:每来一桌的客人,就在那张桌子上安排一个服务员负责,即有多少桌客人就得对应多少个服务员;
而当我们用协程来完成任务时,模式却有所不同了:就安排一个服务员,来吃饭得有一个点餐和等菜的过程,当A在点菜,就去给B服务,B叫了菜在等待,我就去C,当C也在等菜并且A点菜点完了,赶紧到A来服务… …依次类推。
从上面的例子可以看出,想要使用协程,那么我们的任务必须有等待。
当我们要完成的任务是耗时任务时,比如属于IO密集型任务时,我们使用协程来执行任务会节省很多的资源(一个服务员和多个服务员的区别),并且可以极大的利用到系统的资源。
协程,是单线程下的并发,又称微线程,英文名Coroutine。是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置,当程序中存在大量不需要CPU的操作时(IO),适用于协程。【在一个线程中CPU来回切换执行不同的任务,这种现象就是协程】
协程有极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销。
不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
因为协程是一个线程执行,所以想要利用多核CPU,最简单的方法是多进程+协程,这样既充分利用多核,又充分发挥协程的高效率。
那符合什么条件就能称之为协程:
1、必须在只有一个单线程里实现并发
2、修改共享数据不需加锁
3、用户程序里自己保存多个控制流的上下文栈
4、一个协程遇到IO操作自动切换到其它协程
python中对于协程有四个模块,greenlet、gevent、yield和async来实现切换和保存线程。
1.2 yield实现任务切换+保存线程
import timedef func1(): while True: print('func1') yield '返回func1'def func2(): g = func1() print(next(g)) for i in range(3): print(next(g)) time.sleep(3) print('func2')if __name__ == '__main__': start = time.time() func2() stop = time.time() print(stop - start)
yield不能节省IO时间,只是单纯的进行程序切换
# 基于yield并发执行,多任务之间来回切换,这就是个简单的协程的体现,# 但是它能够节省I/O时间吗?不能import timedef consumer(): '''任务1:接收数据,处理数据''' while True: x = yield time.sleep(1) # 发现什么?只是进行了切换,但是并没有节省I/O时间 print('处理了数据:', x)def producer(): '''任务2:生产数据''' g = consumer() next(g) # 找到了consumer函数的yield位置 for i in range(3): # 给yield传值,然后再循环给下一个yield传值,并且多了切换的程序, # 比直接串行执行还多了一些步骤,导致执行效率反而更低了。 print('发送了数据:', i) g.send(i)if __name__ == '__main__': start = time.time() # 基于yield保存状态,实现两个任务直接来回切换,即并发的效果 # PS:如果每个任务中都加上打印,那么明显地看到两个任务的打印是你一次我一次,即并发执行的. producer() # 在当前线程中只执行了这个函数,但是通过这个函数里面的send切换了另外一个任务 stop = time.time() print(stop - start) # 串行执行的方式 print('串行方式') start = time.time() res = producer() consumer() stop = time.time() print(stop - start)
执行效果:
发送了数据: 0处理了数据: 0发送了数据: 1处理了数据: 1发送了数据: 2处理了数据: 23.0110418796539307串行方式发送了数据: 0处理了数据: 0发送了数据: 1处理了数据: 1发送了数据: 2处理了数据: 23.0021891593933105
yield检测不到IO,无法实现遇到IO自动切换。
1.3 greenlet是手动切换
# encoding: utf-8from greenlet import greenletimport time"""使用greenlet + switch实现协程调度"""def func1(): print("开门走进卫生间") time.sleep(3) gr2.switch() # 把CPU执行权交给gr2 print("飞流直下三千尺") time.sleep(3) gr2.switch() passdef func2(): print("一看拖把放旁边") time.sleep(3) gr1.switch() print("疑是银河落九天") passif __name__ == '__main__': gr1 = greenlet(func1) gr2 = greenlet(func2) gr1.switch() # 把CPU执行权先给gr1
执行效果:
开门走进卫生间一看拖把放旁边飞流直下三千尺疑是银河落九天
greenlet只是提供了一种比yield(生成器)更加便捷的切换方式,当切到一个任务执行时如果遇到IO,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。
1.4 Gevent实现自动切换协程(多协程)
协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。
一般在工作中我们都是进程+线程+协程的方式来实现并发,以达到最好的并发效果。
如果是4核的CPU,一般起5个进程,每个进程中20个线程(5倍CPU数量),每个线程可以起500个协程,大规模爬取页面的时候,等待网络延迟的时间的时候,我们就可以用协程去实现并发。并发数量=520500从而达到5000个并发,这是一般一个4个CPU的机器最大的并发数。nginx在负载均衡的时候最大承载量是5w个。
单线程里的这20个任务的代码通常既有计算操作又有阻塞操作,我们完全可以在执行任务1时遇到阻塞,就利用阻塞的时间去执行任务2。。。如此,才能提高效率,这就用到了Gevent模块。
Gevent(自动切换,由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成)。
# encoding: utf-8from gevent import monkey# 将【标准库-阻塞IO实现】替换为【gevent-非阻塞IO实现,即遇到需要等待的IO会自动切换到其它协程monkey.patch_all()import sysimport geventimport requestsimport time'''使用gevent + monkey.patch_all()自动调度网络IO协程'''sys.setrecursionlimit(1000000) # 增加递归深度def get_page_text(url, order): print('No{}请求'.format(order), end=' ') try: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" } resp = requests.get(url, headers=headers) # 发起网络请求,返回需要时间——阻塞IO html = resp.text html_len = len(html) print("%s成功返回:长度为%d" % (url, html_len)) return html_len except Exception as e: print('{}发生错误,{}'.format(url, e)) return 0def gevent_joinall(): # spawn是异步提交任务 gevent.joinall([ gevent.spawn(get_page_text, "http://www.sina.com", order=1), gevent.spawn(get_page_text, "http://www.qq.com", order=2), gevent.spawn(get_page_text, "http://www.baidu.com", order=3), gevent.spawn(get_page_text, "http://www.163.com", order=4), gevent.spawn(get_page_text, "http://www.4399.com", order=5), gevent.spawn(get_page_text, "http://www.sohu.com", order=6), gevent.spawn(get_page_text, "http://www.youku.com", order=7), ]) g_iqiyi = gevent.spawn(get_page_text, "http://www.iqiyi.com", order=8) g_iqiyi.join() # #拿到任务的返回值 print('获取返回值', g_iqiyi.value)if __name__ == '__main__': start = time.time() time.clock() gevent_joinall() end = time.time() print("over,耗时%d秒" % (end - start)) print(time.clock())
执行效果:
No1请求 No2请求 No3请求 No4请求 No5请求 No6请求 No7请求 http://www.sohu.com成功返回:长度为176910http://www.4399.com成功返回:长度为170880http://www.baidu.com成功返回:长度为260247http://www.qq.com成功返回:长度为229706http://www.sina.com成功返回:长度为527564http://www.163.com成功返回:长度为475453http://www.youku.com成功返回:长度为1324990No8请求 http://www.iqiyi.com成功返回:长度为596791获取返回值 596791over,耗时1秒1.3477432
monkey.patch_all() 一定要放到导入requests模块之前,否则gevent无法识别requests的阻塞。
1.5 async实现协程
# encoding: utf-8import asynciofrom functools import partialimport sysimport requestsimport time'''使用gevent + monkey.patch_all()自动调度网络IO协程'''sys.setrecursionlimit(1000000) # 增加递归深度async def get_page_text(url, order): # 使用async创建一个可中断的异步函数 print('No{}请求'.format(order), end=' ') try: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" } # 利用BaseEventLoop.run_in_executor()可以在coroutine中执行第三方的命令,例如requests.get() # 第三方命令的参数与关键字利用functools.partial传入 future = asyncio.get_event_loop().run_in_executor(None, partial(requests.get, url, headers=headers)) resp = await future html = resp.text html_len = len(html) print("%s成功返回:长度为%d" % (url, html_len)) return html_len except Exception as e: print('{}发生错误,{}'.format(url, e)) return 0# 异步函数执行完后回调函数def callback(future): # 这里默认传入一个future对象 print(future.result())# 异步函数执行完后回调函数,可接收多个参数def callback_2(url, future): # 传入值的时候,future必须在最后一个 print(url, future.result())def async_run(): # 使用async创建协程 urls = ["http://www.youku.com", "http://www.sina.com", "http://www.qq.com", "http://www.baidu.com", "http://www.163.com", "http://www.4399.com", "http://www.sohu.com", ] loop_task = [] loop = asyncio.get_event_loop() for i in range(len(urls)): t = asyncio.ensure_future(get_page_text(urls[i], i + 1)) # t = loop.create_task(task(urls[i], i + 1)) t.add_done_callback(callback) # t.add_done_callback(partial(callback_2, urls[i])) loop_task.append(t) print('等待所有async函数执行完成') start = time.time() loop.run_until_complete(asyncio.wait(loop_task)) loop.close() end = time.time() print("over,耗时%d秒" % (end - start))if __name__ == '__main__': async_run()
执行效果:
等待所有async函数执行完成No1请求 No2请求 No3请求 No4请求 No5请求 No6请求 No7请求 http://www.4399.com成功返回:长度为170880170880http://www.sohu.com成功返回:长度为177013177013http://www.baidu.com成功返回:长度为260280260280http://www.qq.com成功返回:长度为229575229575http://www.sina.com成功返回:长度为527564527564http://www.163.com成功返回:长度为475453475453http://www.youku.com成功返回:长度为13356541335654over,耗时1秒
3、关键字:yield
3.1 yield表达式
yield相当于return,只不过return是终结函数并返回一个值,而yield是先把值返回并把函数挂起来,以后还会执行yield以下的语句。
# encoding: utf-8def foo(end_count): print('yield生成器') count = 0 while count < end_count: res = yield count print('接收到的参数', res) if res is not None: count = res else: count += 1if __name__ == '__main__': f = foo(7) # 第一次调用yield函数是预激活, # 即调用函数foo时,只执行yield前面的语句, # 遇到yield就把foo函数挂起来, # 并返回yield后面附带的值 print(next(f)) # 使用next()调用yield函数 for i in range(3): # 第二次调用就开始执行yield后面的语句 print(next(f)) # 使用next()调用yield函数 print('*' * 20) # s使用send给foo函数传值,yield会接收到并赋给res, print(f.send(5)) # send函数中会执行一次next函数 print(next(f)) # 生成不占空间的列表[0,1,2,3,4,5,6,7,8,9] # for i in foo(10): # print(i)
输出:
yield生成器 0 接收到的参数 None 1 接收到的参数 None 2 接收到的参数 None 3 ******************** 接收到的参数 5 5 接收到的参数 None 6
1、第一次ti调用next函数时,进入foo函数,遇到yield就把count=0返回,并把foo函数挂起
2、在for循环中再次调用next函数时,就开始执行yield后面的赋值语句,由于没有接收到值就默认为None,所以res=None
3、然后接着执行赋值语句后面的打印语句和if判断,由于res为None所以执行count +=1,此时count值为1
4、再次遇到yield,返回1,并把foo函数挂起。
5、send函数是可以给yield生成器传参的,执行send函数时会默认执行一次next函数,原理同上。
到这里你可能就明白yield和return的关系和区别了,带yield的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方执行的,所以调用next的时候,生成器并不会从foo函数的开始执行,只是接着上一步停止的地方开始,然后遇到yield后,return出要生成的数,此步就结束。
为什么用这个生成器,是因为如果用List的话,会占用更大的空间,比如说取0,1,2,3,4,5,6............1000
你可能会这样:这个时候range(1000)就默认生成一个含有1000个数的list了,所以很占内存。
这个时候你可以用刚才的yield组合成生成器进行实现
for i in foo(10000): print(i)
但这个由于每次都要调用函数foo,所以比较耗时间。【这就是用时间换空间】
4、关键字:async/await
asyncio 是用来编写 并发 代码的库,使用 async/await 语法。
asyncio 被用作多个提供高性能 Python 异步框架的基础,包括网络和网站服务,数据库连接库,分布式任务队列等等。
asyncio 往往是构建 IO 密集型和高层级 结构化 网络代码的最佳选择。
正常的函数在执行时是不会中断的,所以你要写一个能够中断的函数,就需要添加async关键。
async 用来声明一个函数为异步函数,异步函数的特点是能在函数执行过程中挂起,去执行其他异步函数,等到挂起条件(假设挂起条件是asyncio.sleep(5))消失后,也就是5秒到了再回来执行。
await 用来用来声明程序挂起,比如异步程序执行到某一步时需要等待的时间很长,就将此挂起,去执行其他的异步程序。await 后面只能跟异步程序或有await属性的对象,因为异步程序与一般程序不同。假设有两个异步函数async a,async b,a中的某一步有await,当程序碰到关键字await b()后,异步程序挂起后去执行另一个异步b程序,就是从函数内部跳出去执行其他函数,当挂起条件消失后,不管b是否执行完,要马上从b程序中跳出来,回到原程序执行原来的操作。如果await后面跟的b函数不是异步函数,那么操作就只能等b执行完再返回,无法在b执行的过程中返回。如果要在b执行完才返回,也就不需要用await关键字了,直接调用b函数就行。所以这就需要await后面跟的是异步函数了。在一个异步函数中,可以不止一次挂起,也就是可以用多个await。
可以使用async、await来实现协程的并发,下面以一个爬虫例子来说明:
# encoding: utf-8from gevent import monkeymonkey.patch_all()import geventimport asynciofrom functools import wraps, partialimport timeimport requests# 定义一个查看函数执行时间的装饰器def func_use_time(func): @wraps(func) def inside(*arg, **kwargs): start = time.clock() res = func(*arg, **kwargs) print('***************执行时间*****************', time.clock() - start) return res return insidedef get_page_text(url): # 爬取网站 print(url) try: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" } resp = requests.get(url, headers=headers) # 发起网络请求,返回需要时间——阻塞IO html = resp.text return html except Exception as e: print('{}发生错误,{}'.format(url, e)) return ''class Narmal(): # 正常爬取 def __init__(self, urls): self.urls = urls self.res_dict = {} @func_use_time def run(self): for url in self.urls: res = get_page_text(url) self.res_dict[url] = len(res) print('串行获取结果', self.res_dict)class UseAsyncio(): # 使用async实现协程并发 def __init__(self, urls): self.urls = urls self.res_dict = {} # 定义一个异步函数,执行爬取任务 async def task(self, url): print(url) try: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" } # 利用BaseEventLoop.run_in_executor()可以在coroutine中执行第三方的命令,例如requests.get() # 第三方命令的参数与关键字利用functools.partial传入 future = asyncio.get_event_loop().run_in_executor(None, partial(requests.get, url, headers=headers)) resp = await future html = resp.text self.res_dict[url] = len(html) return html except Exception as e: print('{}发生错误,{}'.format(url, e)) return '' @func_use_time def run(self): loop = asyncio.get_event_loop() tasks = [asyncio.ensure_future(self.task(url)) for url in self.urls] loop.run_until_complete(asyncio.wait(tasks)) loop.close() # 获取async结果 # for task in tasks: # print(task.result()) print('async获取结果', self.res_dict)class UseGevent(): # 使用Gevent实现协程并发 def __init__(self, urls): self.urls = urls self.res_dict = {} def task(self, url): res = get_page_text(url) self.res_dict[url] = len(res) @func_use_time def run(self): gevent.joinall([gevent.spawn(self.task, url) for url in self.urls]) print(self.res_dict)if __name__ == '__main__': urls = ["http://www.sina.com", "http://www.qq.com", "http://www.baidu.com", "http://www.163.com", "http://www.4399.com", "http://www.sohu.com", "http://www.youku.com", ] print("使用正常爬取方式,即串行") Narmal(urls).run() print("使用Asyncio爬取方式,async实现协程并发") UseAsyncio(urls).run() print("使用Gevent爬取方式,实现协程并发") UseGevent(urls).run()
输出:
使用正常爬取方式,即串行 http://www.sina.com http://www.qq.com http://www.baidu.com http://www.163.com http://www.4399.com http://www.sohu.com http://www.youku.com 串行获取结果 {'http://www.sina.com': 539723, 'http://www.qq.com': 227753, 'http://www.baidu.com': 166916, 'http://www.163.com': 483531, 'http://www.4399.com': 172837, 'http://www.sohu.com': 178312, 'http://www.youku.com': 990760} ***************执行时间***************** 1.9352532 使用Asyncio爬取方式,async实现协程并发 http://www.sina.com http://www.qq.com http://www.baidu.com http://www.163.com http://www.4399.com http://www.sohu.com http://www.youku.com async获取结果 {'http://www.4399.com': 172837, 'http://www.163.com': 483531, 'http://www.qq.com': 227753, 'http://www.sohu.com': 178310, 'http://www.baidu.com': 166625, 'http://www.sina.com': 539723, 'http://www.youku.com': 1047892} ***************执行时间***************** 0.951011 使用Gevent爬取方式,实现协程并发 http://www.sina.com http://www.qq.com http://www.baidu.com http://www.163.com http://www.4399.com http://www.sohu.com http://www.youku.com {'http://www.163.com': 483531, 'http://www.4399.com': 172837, 'http://www.sohu.com': 178312, 'http://www.qq.com': 227753, 'http://www.baidu.com': 166760, 'http://www.sina.com': 539723, 'http://www.youku.com': 994926} ***************执行时间***************** 1.0057508
相对来说还是使用async执行效率高些。
【后记】为了让大家能够轻松学编程,我创建了一个公众号【轻松学编程】,里面有让你快速学会编程的文章,当然也有一些干货提高你的编程水平,也有一些编程项目适合做一些课程设计等课题。
也可加我微信【1257309054】,拉你进群,大家一起交流学习。如果文章对您有帮助,请我喝杯咖啡吧!
您觉得文章有帮助,点个在看吧!谢谢^_^