在介绍协程之前,先讲一下异步IO,因为这两者经常交织在一块。
当我们发起一个IO请求,比如读写文件,网络请求这些比较耗时的时候,我们就可以使用异步IO来切换去做其他事情而不用在这傻傻等待这些操作结束。而在Python中,异步IO是通过asyncio来实现的,这个稍后会介绍。
为了解决这些IO密集型的任务,比方说我们要在浏览器中打开多个网页,我们会使用多进程或者多线程。虽然使用线程的切换比进程切换更加节省资源以及节省时间。但是由于Python中有GIl锁,也就是每个时候都只有一个线程在工作,那么在这样的情况下,就无法发挥出多线程的优势。
而此时协程的横空出世就可以解决这问题。
协程也叫作,轻量级的线程,它比线程更加节省资源,并且不需要CPU调度上下文的切换,而是由程序员自己来控制。此外,协程没有锁的操作也就不需要进行同步上的控制,并且协程理论上来说是无限的,而线程的个数受电脑CPU的限制。
下面就来Python中的看看协程使用,这里使用了生产者消费者模型来模拟协程的切换:
def consumer():
print("[CONSUMER] start")
signal = 'start'
while True:
n = yield signal
if not n:
print("n is empty")
continue
print("[CONSUMER] Consumer is consuming %s" % n)
signal = "ok" #返回while 回到开头的yield signal
def producer(c):
# 启动generator, 第一次使用要用send(None)启动生成器
value = c.send(None)
print(value)
n = 0
while n < 3:
n += 1
print("[PRODUCER] Producer is producing %d" % n)
result = c.send(n)
print('[PRODUCER] Consumer return: %s' % result)
# 关闭generator
c.close()
# 创建生成器
c = consumer()
# 传入generator
producer(c)
协程中关键的是切换,yield负责暂停,send负责传输变量并且重启生成器或者使用next函数。当需要切换的时候,我们就使用yield这个关键字,使用yield后,这个变量就会暂停,并且先不会进行赋值。从根本上看,yield是流程控制的工具,可以实现协作式多任务,这也是后面讲解异步IO的基础。
需要注意的是,我们一开始要是用send(None)来启动生成器,如果缺少这句语句,就会报错。
yield和yield from的区别
我们会时不时地遇到yield和yield from,那么这两者之间有什么区别呢?我们来看下代码吧
def test1():
yield range(3)
def test2():
yield from range(3)
it1 = test1()
it2 = test2()
for x in it1:
print(x)
for x in it2:
print(x)
'''
输出结果
range(0, 3)
0
1
2
'''
从上面的输出结果可以看出,yield返回的是range()这可迭代对象,而yield feom就解析了range()对象,将其中的每一个元素都提取出来了。
async与await
在Python中,通过使用async这个异步IO库使用事件来循环驱动的协程去实现并发就可以将这个函数定义为协程函数。
而在比较耗时的地方就可以采用await,后面跟的是一个 awaitable 对象,也就是协程对象,它的返回值是 awaitable 对象的结果。那么什么是await对象?任何一个实现了await()方法的对象或者协程对象都是await对象。当执行到await时,程序的运行就会交给后面的 awaitable 对象。
async关键字将一个函数声明为协程函数,函数执行时返回一个协程对象。
await关键字将暂停协程函数的执行,等待异步IO返回结果。
import asyncio,random
async def genObj(alist):
while len(alist) > 0:
c = random.randint(0, len(alist)-1)
print(alist.pop(c))
await asyncio.sleep(1)
intlist=[1,2,5,6]
t1 = genObj(intlist)
print(t1)
这样,打印出来的结果就是协程对象(<coroutine object genObj at 0x000002618EDE0728>)
下面再看一下异步IO中的例子,asyncio.get_event_loop()获取一个事件循环的对象,然后初始化任务列表,类似多线程中的格式。而其中的asyncio.sleep是模拟IO操作,这样当程序进行休眠的时候,也不会造成阻塞,遇到await后则释放当前的控制权。
import threading
async def hello(index):
print('Hello world! index=%s, thread=%s' % (index, threading.currentThread()))
await asyncio.sleep(1)
print('Hello again! index=%s, thread=%s' % (index, threading.currentThread()))
loop = asyncio.get_event_loop()
tasks = [hello(1), hello(2)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
小结
在Python中,爬虫工作经常会用到异步IO,因为网络请求都是IO密集型的任务,而且Python中有Tornado框架,这个框架也是基于异步IO的。大家感兴趣的可以看看Tornado的官方文档。