事件驱动与 阻塞IO、非阻塞IO、IO多路复用、异步IO
转载文章:事件驱动与 阻塞IO、非阻塞IO、IO多路复用、异步IO
asyncio
之前爬虫使用的是requests+多线程/多进程,后来随着前几天的深入了解,才发现,对于爬虫来说,真正的瓶颈并不是CPU的处理速度,而是对于网页抓取时候的往返时间,因为如果采用requests+多线程/多进程,他本身是阻塞式的编程,所以时间都花费在了等待网页结果的返回和对爬取到的数据的写入上面。而如果采用非阻塞编程,那么就没有这个困扰。这边首先要理解一下阻塞和非阻塞的区别
1.阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,CPU不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
2.对于非阻塞则不会挂起,直接执行接下去的程序,返回结果后再回来处理返回值。
python 3.4开始就支持异步IO编程,提供了asyncio库,但是3.4中采用的是@asyncio.coroutine和
yield from这和原来的generator关键字yield不好区分,在3.5中,采用了async(表示携程)和await关键字,这样就好区分多了。3.6正式加入stdlib中。
这边用一个简单的官方例子来说明async和await的执行顺序:
import asyncio async def compute(x, y): print("Compute %s + %s ..." % (x, y)) await asyncio.sleep(1.0) return x + y async def print_sum(x, y): result = await compute(x, y) print("%s + %s = %s" % (x, y, result)) loop = asyncio.get_event_loop() loop.run_until_complete(print_sum(1, 2)) loop.close()
如果不使用async和await,这个程序的运行顺序也很好理解
1.print_sum(1,2)将参数1,2传递给函数print_sum
2.先执行第一句,result = compute(x,y),
3.将1,2传递给compute函数,compute函数收到参数
4.先执行 print("Compute %s + %s ..." % (x, y))打印出Compute 1 + 2 ...
5.执行sleep(1.0)程序挂起一秒
6.返回1+2的值3
7.print_sum的result收到返回值3,执行print("%s + %s = %s" % (x, y, result)),打印出1 + 2 = 3
8.程序结束
如果采用异步的话,他执行顺序是怎么样的呢?
下图是他官方文档的说明:
分析上面这段代码,首先async def创建一个coroutine object,get_event_loop创建一个消息环,接下去,run_until_complete()接受coroutine object对象,通过隐式转换(ensure_future())将其包装成一个future,这边是一个task对象(内部实现机制我其实也并不是非常确定,大致是这样,比如如何从future转换成task??)
1.get_event_loop()创建消息环
2.print_sum返回一个携程对象(coroutine object)
3.print_sum(1,2)接受参数
4.result = await compute(x,y),此时程序挂起,直接调用compute(1,2)
5.打印出Compute 1 + 2 ...
6.执行sleep(1.0) 由于使用await,所以sleep(1.0)被挂起,直接执行下一句
7.这边由于只有一个task,即print_sum(1,2),因此没有其他task可以执行,所以result等待compute(x,y)的返回值,等待1秒以后,返回1+2的值3
然后执行print("%s + %s = %s" % (x, y, result))
其实上面的程序可能还不能很明白async def和 await的调用过程到底是怎么样,稍微修改一下上面的程序就能更加明白他的执行过程了。
import asyncio async def compute(x, y): print("Compute %s + %s ..." % (x, y)) await asyncio.sleep(10.0) return x + y async def print_sum(x, y): result = await compute(x, y) print("%s + %s = %s" % (x, y, result)) loop = asyncio.get_event_loop() tasks = [print_sum(1,2),print_sum(3,4)] loop.run_until_complete(asyncio.wait(tasks)) loop.close()
把代码修改成这样,其他不变,多了一个task,为了更加直观,我把sleep(1.0)改成了sleep(10.0)
此时程序的执行结果是
Compute 3 + 4 ... Compute 1 + 2 ... 暂停10秒左右 3 + 4 = 7 1 + 2 = 3
非常需要注意的就是这边暂停的时间,是10秒左右,不是20秒,所以他执行顺序是对于task
先执行print_sum(3,4)(这边tasks额执行顺序我不是非常理解,我多加了很多task测验,发现都是从第二个开始执行,然后最后执行第一个task,没懂是为什么)将3,4扔给compute,在compute中,sleep(10.0)挂起,此时不等待sleep(10.0)执行完毕,直接执行tasks中的另一个task,print_sum(1,2),将1,2扔给compute,在compute中,sleep(10.0)挂起,由于已经没有其他的task,所以等待第一个sleep(10.0)执行完毕以后返回3+4的结果为7,执行result = await compute(3, 4)后面的程序,即打印出3 + 4 = 7执行完毕以后,第二个sleep(10.0)也差不多返回了,因此回到原来挂起的result = await compute(1, 2),打印出1 + 2 = 3.所有task执行完毕,loop complete并且close。