1. 什么是协程?
协程(coroutine),又称微线程。协程不是线程也不是进程,它的上下文关系切换不是由CPU控制,一个协程由当前任务切换到其他任务由当前任务来控制。一个线程可以包含多个协程,对于CPU而言,不存在协程这个概念,它是一种轻量级用户态线程(即只针对用户而言)。协程拥有自己的寄存器上下文和栈,协程调度切换到其他协程时,将寄存器上下文和栈保存,在切回到当前协程的时候,恢复先前保存的寄存器上下文和栈。
2. 在编程中为什么要使用协程?
使用协程的好处:(1)CPU无需负担上下文的开销;(2)不需加锁(多个线程操作数据时得加锁);(3)由程序员切换控制流,方便编程;(4)高并发、高扩展、低成本(一个CPU支持上万的协程都不是问题)。
当然,任何事物有优点必有缺点。协程得缺点:(1)协程自己无法利用CPU多核资源(除非与多进程或者多线程配合);(2)遇到阻塞操作会使整个程序阻塞。
例一(使用yield实现在任务间的切换):
1 import time 2 3 def func1(name): 4 print("----func1 start...----") 5 for i in range(6): 6 temp = yield #每次遇到yield,func1在此处阻塞,直到temp接收到func2中con.send()传来的值 7 print("%s in the func1" % (str(temp))) 8 time.sleep(1) 9 10 11 def func2(): 12 print("----func2 start...----") 13 con.__next__() #此处开始真正的func1的调用 14 for i in range(5): 15 con.send(i+1) 16 print("%s in the func2" % i) 17 18 19 if __name__ == '__main__': 20 con = func1(1) #在有yield的函数中此处不是真正的函数调用,打印con便可知道 21 # print(con) 22 p = func2()
注:例一严格来说不能算是协程,只是实现了两个任务之间的切换。
3. 既然例一不能算多协程,难么在python中应该如何使用协程?
greenlet是一个用C实现的协程模块,相比与python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator(例一中的con=func1(1)就是做这个操作)。
例二:
1 import greenlet 2 3 def func1(): 4 for i in range(1,6): 5 print(i) 6 g2.switch() #切换到g2 7 8 def func2(): 9 words = ['a', 'b', 'c', 'd', 'e'] 10 for w in words: 11 print(w) 12 g1.switch() #切换到g1 13 14 g1 = greenlet.greenlet(func1) 15 g2 = greenlet.greenlet(func2) 16 g1.switch() #切换到g1
注:使用greenlent可以很简单的进行多任务之间的切换,但是程序运行最耗时的便是I/O操作,要使用协程实现高并发,应当是一旦遇到I/O操作就切换到其他任务,等I/O操作完成后在切回到当前任务(这个过程应当是自动的)。
4. 那么在python中,如何让任务遇到I/O操作就切换?
我们使用第三方库gevent来实现。
gevent的官方定义:gevent is a coroutine -based Python networking library that uses greenlet to provide a high-level synchronous API on top of the libev event loop.
例三(gevent的简单使用):
1 import gevent 2 import time 3 4 def func1(): 5 print("func1: start.....") 6 # Put the current greenlet to sleep for at least *seconds*.(模拟I/O操作,任务在此处自动切换) 7 gevent.sleep(3) 8 print("func1: end") 9 10 def func2(): 11 print("func2: start.....") 12 gevent.sleep(0.5) 13 print("func2: end") 14 15 start_time = time.time() 16 # joinall(greenlets, timeout=None, raise_error=False, count=None) 17 # Wait for the ``greenlets`` to finish. 18 # :return: A sequence of the greenlets that finished before the timeout (if any)expired. 19 gevent.joinall([gevent.spawn(func1), 20 gevent.spawn(func2)]) 21 # spawn(cls, *args, **kwargs) 22 # Create a new :class:`Greenlet` object and schedule it to run ``function(*args, **kwargs)``. 23 # This can be used as ``gevent.spawn`` or ``Greenlet.spawn``. 24 25 print("cost:", time.time()-start_time) 26 # 通过计算程序运行的时间可以发现程序确实是以单线程达模拟出了多任务并行的操作。
例四(gevent和urllib配合同时下载多个网页):
1 import urllib.request 2 import gevent,time 3 import gevent.monkey 4 5 def func(url="", filename=""): 6 print("Download:%s" % url) 7 result = urllib.request.urlopen(url) #请求打开一个网页 8 data = result.read() #读取内容 9 with open(filename, 'wb') as fp: #写入文档 10 fp.write(data) 11 print("Finish:%s" % url) 12 13 if __name__ == "__main__": 14 # Do all of the default monkey patching (calls every other applicablefunction in this module). 15 # 相当与做一个标记,做完此操作gevent就可以检测到此程序中所有的I/O操作 16 gevent.monkey.patch_all() 17 18 async_time = time.time() 19 gevent.joinall([ 20 gevent.spawn(func, "http://www.cnblogs.com/God-Li/p/7774497.html", "7774497.html"), 21 gevent.spawn(func, "http://www.gevent.org/", "gevent.html"), 22 gevent.spawn(func, "https://www.python.org/", "python.html"), 23 ]) 24 print("async download cost:", time.time()-async_time) 25 26 start_time = time.time() 27 func("http://www.cnblogs.com/God-Li/p/7774497.html", "7774497.html") 28 func("http://www.gevent.org/", "gevent.html") 29 func("https://www.python.org/", "python.html") 30 print("download cost:", time.time()-start_time)
注:对上例代码稍加改造,加上对html源码的解析功能,就可以实现一个简单的多并发爬虫。
对python --- 网络编程Socket中例二的socket_server2使用gevent改造就可以使其成为一个大并发的socket server。
例五(使用gevent实现并发的socket server):
1 #服务端 2 import socket 3 import gevent 4 import gevent.monkey 5 6 gevent.monkey.patch_all() 7 8 def request_handler(conn): 9 10 ''' 11 Wait for an incoming connection. Return a new socket 12 representing the connection, and the address of the client. 13 ''' 14 while True: 15 # print("ok") 16 data = conn.recv(1024) #接收信息,写明要接收信息的最大容量,单位为字节 17 print("server recv:", data) 18 conn.send(data.upper()) #对收到的信息处理,返回到客户端 19 20 21 22 if __name__ == "__main__": 23 address = ("localhost", 6666) # 写明服务端要监听的地址,和端口号 24 server = socket.socket() # 生成一个socket对象 25 server.bind(address) # 用socket对象绑定要监听的地址和端口 26 server.listen() # 开始监听 27 28 while True: 29 conn, addr = server.accept() # 等带新连接接入服务端,返回一个新的socket对象和地址,地址格式同前面格式 30 gevent.spawn(request_handler, conn) 31 32 server.close() # 关闭服务端
注:可使用python --- 网络编程Socket中例二的socket_client2进行测试。