前面的文章提到过,python使用多线程,会因为GIL的原因导致多线程的使用效率低下,甚至比单个线程的处理速度还慢。然而在python编程中,为了解决多线程之间上下文切换的开销,以及增加线程控制的灵活性,python引入了协程。本文我们就来说一说python协程的特点和使用方法。
定义:协程(Coroutine),又称微线程。协程的作用,是在执行函数A时,可以随时中断,去执行函数B,然后中断继续执行函数A(可以自由切换)。但这一过程并不是函数调用(没有调用语句),这一整个过程看似像多线程,然而协程只有一个线程执行。
无论多线程和多进程,IO的调度更多取决于系统,而协程的方式,调度来自用户,用户可以在函数中yield一个状态。使用协程可以实现高效的并发任务。
Python的在3.4中引入了协程的概念,可是这个还是以生成器对象为基础,3.5则确定了协程的语法。
实现协程通常可以引入一些python库来实现,其中最常用的就是asyncio,当然也有一些其他的如tornado和gevent都实现了类似的功能。这里我们就来了解一下asyncio
协程通过 async/await 语法进行声明,是编写异步应用的推荐方式。我们先来了解asyncio的几个概念名词:
event_loop 事件循环:程序开启一个无限的循环,程序员会把一些函数注册到事件循环上。当满足事件发生的时候,调用相应的协程函数。
coroutine 协程:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。
task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态。
future: 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别
async/await 关键字:python3.5 用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口。
为了解释上面的名词,我们一步步来实现简单的异步编程:
1、定义一个协程
import time
import asyncio
# 定义一个协程并创建一个事件循环
loop = asyncio.get_event_loop()
# 协程对象coroutine需要在事件循环里面才能执行
loop.run_until_complete(coroutine)
2、协程对象
async def sync_func(x):
print('Waiting: ', x)
# 实例化一个协程对象
coroutine = sync_func(2)
由上面可以得出,用async修饰的方法是一个协程对象,协程对象需要在协程的事件循环里面才能运行。
3、创建一个task
task = asyncio.ensure_future(coroutine)
loop.run_until_complete(task)
事件循环除了可以运行协程对象,也可以运行任务。
在这里task只是对协程对象coroutine进行了封装,task还可以绑定协程对象执行后的回调,甚至还可以封装成list数组等,逐个进行事件循环调用。
4、task绑定回调函数
def callback(x):
print(x)
task = asyncio.ensure_future(coroutine)
task.add_done_callback(functools.partial(callback, 2))
loop.run_until_complete(task)
其中callback是回调函数,执行完协程之后会执行回调函数。绑定回调也是用task封装协程对象的一个好处
5、协程的阻塞和await
async def sync_func(x):
print('Waiting: ', x)
await asyncio.sleep(x)
return 'Done after {}s'.format(x)
coroutine = sync_func(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)
loop.run_until_complete(task)
如上所述,如果协程对象函数里面带有await,是可以实现协程的运行阻塞的。上面例子执行结果是print之后sleep了2秒,这期间可以让出控制器,loop可以去调用别的协程,然后再回来执行return,return的结果可以用task.result()来获取。
6、多个协程并发执行
async def sync_func(x):
print('Waiting: ', x)
await asyncio.sleep(x)
return 'Done after {}s'.format(x)
start = now()
coroutine1 = sync_func(1)
coroutine2 = sync_func(2)
tasks = [
asyncio.ensure_future(coroutine1),
asyncio.ensure_future(coroutine2)
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
上述例子执行结果为:协程1print之后阻塞,再协程2print后也阻塞,最后协程1return,协程2再return。
7、协程停止:启动事件循环之后,马上ctrl+c,会触发run_until_complete的执行异常 KeyBorardInterrupt。然后通过循环asyncio.Task取消future。
try:
loop.run_until_complete(asyncio.wait(tasks))
except KeyboardInterrupt as e:
loop.stop()
loop.run_forever()
finally:
loop.close()
1、当一个程序变得很大而且复杂时,将其划分为子程序,每一部分实现特定的任务是个不错的方案。子程序不能单独执行,只能在主程序的请求下执行,主程序负责协调使用各个子程序。协程就是子程序的泛化。和子程序一样的事,协程只负责计算任务的一步;和子程序不一样的是,协程没有主程序来进行调度。
2、异步爬虫
很多关心协程的朋友,大部分是用其写爬虫,这是因为协程能很好的解决IO阻塞问题。那么对于异步爬虫的需求,使用协程的方法大致如下:
(1)grequests;
(2)爬虫模块+gevent;
(3)aiohttp;
(4)scrapy框架+asyncio模块
3、协程池
类似于gevent,我们可以先创建好协程,放入一个协程池中,每次有任务请求的时候都由协程去执行,主线程进行统一管理和调度,这在一下I/O密集型的数据清洗等工作中可以提高很多效率。
1、执行效率高,因为子程序切换(函数)不是线程切换,由程序自身控制,没有切换线程的开销。所以与多线程相比,线程的数量越多,协程性能的优势越明显;
2、不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在控制共享资源时也不需要加锁,因此执行效率高很多。
3、缺点:无法利用多核资源,协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上。
4、进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
参考链接:
https://thief.one/2017/02/20/Python%E5%8D%8F%E7%A8%8B/
https://www.jianshu.com/p/7690edfe9ba5