在引出協成概念之前先說說python的進程和線程。
進程:
進程是正在執行程序實例。執行程序的過程中,內核會講程序代碼載入虛擬內存,為程序變量分配空間,建立 bookkeeping 數據結構,來記錄與進程有關的信息,
比如進程 ID,用戶 ID 等。在創建進程的時候,內核會為進程分配一定的資源,並在進程存活的時候不斷進行調整,比如內存,進程創建的時候會占有一部分內存。
進程結束的時候資源會釋放出來,來讓其他資源使用。我們可以把進程理解為一種容器,容器內的資源可多可少,但是在容器內的程序只能使用容器內的東西。因此啟動
進程的時候會比較慢,尤其是windows,尤其是多進程的時候(最好是在密集性運算的時候啟動多進程)
線程:
一個進程中可以執行多個線程。多個線程共享進程內的資源。所以可以將線程可以看成是共享同一虛擬內存以及其他屬性的進程。
線程相對於進程的優勢在於同一進程下的不同線程之間的數據共享更加容易。
在說到線程的時候說說GIL(全局解釋性鎖 GLOBAL INTERPRETER LOCK),GIL 的存在是為了實現 Python 中對於共享資源訪問的互斥。而且是非常霸道的解釋器級別的互斥。在 GIL 的機制下,一個線程訪問解釋器之后,其他的線程就需要等待這個線程釋放之后才可以訪問。這種處理方法在單處理器下面並沒有什么問題,單處理器的本質是串行執行的。但是再多處理器下面,這種方法會導致無法利用多核的優勢。Python 的線程調度跟操作系統的進程調度類似,都屬於搶占式的調度。一個進程執行了一定時間之后,發出一個信號,操作系統響應這個時鍾中斷(信號),開始進程調度。而在 Python 中,則通過軟件模擬這種中斷,來實現線程調度。比如:對全局的num做加到100的操作,可能在你加到11的時候,還沒加完,則CPU就交給另一個線程處理,所以最后的結果可能比100會小或者比100會大。
簡單的說說進程和線程的幾點關系
1、啟動一個進程至少會有一個線程
2、修改主線程的數據會影響到子線程的數據,因為他們之間內存是共享的,修改主進程不會影響到子進程的數據,兩個子進程之間是相互獨立的,如果要實現子進程間的通信,可以利用中間件,比如multiprocessing的Queue。
如:
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 #進程之間的通信 5 from multiprocessing import Process,Queue 6 7 def f(qq): 8 #在子進程設置值,本質上是子進程pickle數據序列化到公共的地方 9 qq.put(['hello',None,123]) 10 11 12 if __name__ == '__main__': 13 q = Queue() 14 t = Process(target=f,args=(q,)) 15 t.start() 16 #從父進程中取出來,本質上是父進程pickle從公共的地方把數據反序列化出來 17 print q.get() 18 t.join()
3、新的線程很容易被創建,但是新的進程需要對其父進程進行一次克隆
4、一個線程可以操作和控制同一個進程里的其他線程,但進程只能操作其子進程。
明白了進程和線程的概念之后,說說協成。
協程:
協程,又稱微線程。英文名Coroutine。
協程最大的優勢就是協程極高的執行效率。因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。
第二大優勢就是不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。
因為協程是一個線程執行,那怎么利用多核CPU呢?最簡單的方法是多進程+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的性能。

用yield來實現傳統的生產者-消費者模型是一個線程寫消息,一個線程取消息,通過鎖機制控制隊列和等待。
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 import time 5 6 def consumer(): 7 r = '' 8 while True: 9 n = yield r 10 if not n: 11 return 12 print('Consume running %s...' % n) 13 time.sleep(1) #遇到阻塞到produce執行 14 r = '200 OK' 15 16 def produce(c): 17 c.next() #啟動迭代器 18 n = 0 19 while n < 5: 20 n = n + 1 21 print('[Produce] running %s...' % n) 22 r = c.send(n) #到consumer中執行 23 print('[Consumer] return: %s' % r) 24 c.close() 25 26 if __name__=='__main__': 27 c = consumer() #迭代器 28 produce(c) 29 30 執行結果: 31 [Produce] running 1... 32 Consume running 1... 33 [Consumer] return: 200 OK 34 [Produce] running 2... 35 Consume running 2... 36 [Consumer] return: 200 OK 37 [Produce] running 3... 38 Consume running 3... 39 [Consumer] return: 200 OK 40 [Produce] running 4... 41 Consume running 4... 42 [Consumer] return: 200 OK 43 [Produce] running 5... 44 Consume running 5... 45 [Consumer] return: 200 OK
其實python有個模塊封裝了協程功能,greenlet.來看代碼。
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 #封裝好的協成 5 from greenlet import greenlet 6 7 def test1(): 8 print "test1:",11 9 gr2.switch() 10 print "test1:",12 11 gr2.switch() 12 13 def test2(): 14 print "test2:",13 15 gr1.switch() 16 print "test2:",14 17 18 19 gr1 = greenlet(test1) 20 gr2 = greenlet(test2) 21 gr1.switch() 22 23 執行結果: 24 test1: 11 25 test2: 13 26 test1: 12 27 test2: 14
這個還的人工切換,是不是覺得太麻煩了,不要捉急,python還有一個自動切換比greenlet更強大的gevent。
其原理是當一個greenlet遇到IO操作時,比如訪問網絡,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。
由於IO操作非常耗時,經常使程序處於等待狀態,有了gevent為我們自動切換協程,就保證總有greenlet在運行,而不是等待IO。直接上代碼
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 #協成的自動切換 5 import gevent 6 import time 7 8 def func1(): 9 print('\033[31;1m 正在執行 111...\033[0m') 10 gevent.sleep(2) 11 print('\033[31;1m 正在執行 444...\033[0m') 12 13 14 def func2(): 15 print('\033[32;1m 正在執行 222...\033[0m') 16 gevent.sleep(3) #阻塞3秒,所以自動切換到func1,執行完func1后 再切換回來 17 print('\033[32;1m 正在執行 333...\033[0m') 18 19 start_time = time.time() 20 gevent.joinall([ 21 gevent.spawn(func1), 22 gevent.spawn(func2), 23 # gevent.spawn(func3), 24 ]) 25 end_time = time.time() 26 27 #程序總共花費3秒執行 28 print "spend",(end_time-start_time),"second" 29 30 執行結果: 31 正在執行 111... 32 正在執行 222... 33 正在執行 444... 34 正在執行 333... 35 總耗時: 36 spend 3.00936698914 second
下面我們用greenlet來實現一個socket多線程處理數據的功能。不過需要安裝一個monkey補丁,請自行安裝吧。
client端:
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 5 from socket import * 6 7 ADDR, PORT = 'localhost', 8001 8 client = socket(AF_INET,SOCK_STREAM) 9 client.connect((ADDR, PORT)) 10 11 while 1: 12 cmd = raw_input('>>:').strip() 13 if len(cmd) == 0: continue 14 client.send(cmd) 15 data = client.recv(1024) 16 print data 17 #print('Received', repr(data)) 18 19 client.close()
server端:
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 5 import sys 6 import socket 7 import gevent 8 from gevent import monkey 9 monkey.patch_all() 10 11 def server(port): 12 sock = socket.socket() 13 sock.bind(('127.0.0.1', port)) 14 sock.listen(500) 15 while 1: 16 conn, addr = sock.accept() 17 #handle_request(conn) 18 gevent.spawn(handle_request, conn) 19 20 21 def handle_request(conn): 22 try: 23 while 1: 24 data = conn.recv(1024) 25 if not data: 26 break 27 print("recv:",data) 28 conn.send(data) 29 30 except Exception as ex: 31 print(ex) 32 finally: 33 conn.close() 34 35 if __name__ == '__main__': 36 server(8001)
以上代碼可以自行多開幾個客戶端,然后執行看看,是不是很酷,無論客戶端輸入什么,服務端都能實時接收到。
OVER!
