1.总则
多进程可以实现真正的并行,但进程间无法进行直接通信且占用资源较多。多线程的使用代价相对多进程较小,但为了解决数据安全问题引入了锁的机制。这又使得多线程并发度降低,同时,使用锁还可能造成死锁。
在此背景下,协程出现了。协程是单线程,是任务级的切换。相比线程和进程切换,代价小得多,并发性能也很高。
2.实现原理
上面提到,协程是任务级的切换,具体地说就是函数级的切换。这句话展开有两个要点。①要有一个负责切换函数执行的循环;②函数要能暂停和重启。函数要实现暂停和重启,就要引入生成器的概念。
(1)生成器——函数暂停和重启
简单讲,函数内部使用了关键字yield就是生成器。
def gen_fun():
yield 1
yield 2
g = gen_fun()
print(next(g))
print(next(g))
# 输出:1
2
生成器函数和普通函数相比,首先是使用了yield。yield和return都可以返回值。但return只能有一个,yield可以有多个;return是函数的结束,yield只是暂停,下一次调用会从上一次暂停的地方继续执行。为了展现“继续执行”,我们把上面的例子补充下:
def gen_fun():
yield 1
yield 2
yield 3
g = gen_fun()
print(next(g))
print('-------上次执行-------')
for num in g:
print(num)
# 输出:1
-------上次执行-------
2
3
可以看到,for循环的值是从2开始的,因为第一个yield已经被next()执行了。有了生成器,函数的暂停和重启也就实现了。yield除了可以返回值也可以接收值(关于更多生成器的知识,单独总结)
yield既可以实现生成器,也可以实现协程。为了使语义更明确,一般使用async-await实现协程。具体例子放在下面一起呈现。
(2)事件循环
可以使用while循环去遍历任务,监听,某任务有返回,则获取返回值做下一步操作(相对复杂)。也可以直接使用python(3.5以后)自带的异步IO——asyncio创建循环。且,其使用方式和多线程、多进程相似,保证了接口的一致性。
async def downloader(url):
print('准备从{}下载内容'.format(url))
# 模拟下载,不能使用time.sleep,会阻塞单线程
await asyncio.sleep(3)
return '这是从{}下载的内容,'.format(url)
async def handle(url):
html = await downloader(url)
print(html+'这是下载后进一步的处理')
return html
if __name__ == "__main__":
urls = ["http://www.baidu.com", "http://www.sina.com"]
# 创建事件循环
loop = asyncio.get_event_loop()
start_time = time.time()
tasks = [handle(url) for url in urls]
loop.run_until_complete(asyncio.wait(tasks))
print("耗时:", time.time()-start_time)
# 输出:准备从http://www.sina.com下载内容
准备从http://www.baidu.com下载内容
这是从http://www.sina.com下载的内容,这是下载后进一步的处理
这是从http://www.baidu.com下载的内容,这是下载后进一步的处理
耗时: 3.000380754470825
从最终输出打印的顺序和耗时就可以看出,协程是如何运作并实现并发的。当第一个url传到downloader的时候,我使用asyncio.sleep(3)模拟等待下载的时间。而线程并没有阻塞在这里,而是在等待的时候暂停了这个函数的执行(没有继续往下执行),并提交了第二个url请求。因此,先打印了两个“准备从xx下载内容”,当第一个url请求返回了结果,调度又切换回来继续执行。返回结果并交由handle处理。最后,从耗时也可以看出,两个请求都等待3s,使用协程的总耗时却不是6s。因此,我们使用协程实现了并发。
3.注意事项
协程里不能使用阻塞式代码(如time.sleep)和阻塞式第三方库(requests,pymysql)
关于asyncio更多知识,如其他创建任务方式,聚合任务,取消任务,获取任务返回等知识,单独总结。