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進行測試。
