在引出協程概念之前先說說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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#!/usr/bin/env python # -*- coding:utf-8 -*- #程式之間的通訊 from multiprocessing import Process,Queue def f(qq): #在子程式設定值,本質上是子程式pickle資料序列化到公共的地方 qq.put(['hello',None,123]) if __name__ == '__main__': q = Queue() t = Process(target=f,args=(q,)) t.start() #從父程式中取出來,本質上是父程式pickle從公共的地方把資料反序列化出來 print q.get() t.join() |
3、新的執行緒很容易被建立,但是新的程式需要對其父程式進行一次克隆
4、一個執行緒可以操作和控制同一個程式裡的其他執行緒,但程式只能操作其子程式。
明白了程式和執行緒的概念之後,說說協成。
協程:
協程,又稱微執行緒。英文名Coroutine。
協程最大的優勢就是協程極高的執行效率。因為子程式切換不是執行緒切換,而是由程式自身控制,因此,沒有執行緒切換的開銷,和多執行緒比,執行緒數量越多,協程的效能優勢就越明顯。
第二大優勢就是不需要多執行緒的鎖機制,因為只有一個執行緒,也不存在同時寫變數衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多執行緒高很多。
因為協程是一個執行緒執行,那怎麼利用多核CPU呢?最簡單的方法是多程式+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的效能。
用yield來實現傳統的生產者-消費者模型是一個執行緒寫訊息,一個執行緒取訊息,通過鎖機制控制佇列和等待。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#!/usr/bin/env python # -*- coding:utf-8 -*- import time def consumer(): r = '' while True: n = yield r if not n: return print('Consume running %s...' % n) time.sleep(1) #遇到阻塞到produce執行 r = '200 OK' def produce(c): c.next() #啟動迭代器 n = 0 while n < 5: n = n + 1 print('[Produce] running %s...' % n) r = c.send(n) #到consumer中執行 print('[Consumer] return: %s' % r) c.close() if __name__=='__main__': c = consumer() #迭代器 produce(c) 執行結果: [Produce] running 1... Consume running 1... [Consumer] return: 200 OK [Produce] running 2... Consume running 2... [Consumer] return: 200 OK [Produce] running 3... Consume running 3... [Consumer] return: 200 OK [Produce] running 4... Consume running 4... [Consumer] return: 200 OK [Produce] running 5... Consume running 5... [Consumer] return: 200 OK |
其實python有個模組封裝了協程功能,greenlet.來看程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#!/usr/bin/env python # -*- coding:utf-8 -*- #封裝好的協成 from greenlet import greenlet def test1(): print "test1:",11 gr2.switch() print "test1:",12 gr2.switch() def test2(): print "test2:",13 gr1.switch() print "test2:",14 gr1 = greenlet(test1) gr2 = greenlet(test2) gr1.switch() 執行結果: test1: 11 test2: 13 test1: 12 test2: 14 |
這個還的人工切換,是不是覺得太麻煩了,不要捉急,python還有一個自動切換比greenlet更強大的gevent。
其原理是當一個greenlet遇到IO操作時,比如訪問網路,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。
由於IO操作非常耗時,經常使程式處於等待狀態,有了gevent為我們自動切換協程,就保證總有greenlet在執行,而不是等待IO。直接上程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#!/usr/bin/env python # -*- coding:utf-8 -*- #協成的自動切換 import gevent import time def func1(): print('\033[31;1m 正在執行 111...\033[0m') gevent.sleep(2) print('\033[31;1m 正在執行 444...\033[0m') def func2(): print('\033[32;1m 正在執行 222...\033[0m') gevent.sleep(3) #阻塞3秒,所以自動切換到func1,執行完func1後 再切換回來 print('\033[32;1m 正在執行 333...\033[0m') start_time = time.time() gevent.joinall([ gevent.spawn(func1), gevent.spawn(func2), # gevent.spawn(func3), ]) end_time = time.time() #程式總共花費3秒執行 print "spend",(end_time-start_time),"second" 執行結果: 正在執行 111... 正在執行 222... 正在執行 444... 正在執行 333... 總耗時: spend 3.00936698914 second |
下面我們用greenlet來實現一個socket多執行緒處理資料的功能。不過需要安裝一個monkey補丁,請自行安裝吧。
client端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#!/usr/bin/env python # -*- coding:utf-8 -*- from socket import * ADDR, PORT = 'localhost', 8001 client = socket(AF_INET,SOCK_STREAM) client.connect((ADDR, PORT)) while 1: cmd = raw_input('>>:').strip() if len(cmd) == 0: continue client.send(cmd) data = client.recv(1024) print data #print('Received', repr(data)) client.close() |
server端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#!/usr/bin/env python # -*- coding:utf-8 -*- import sys import socket import gevent from gevent import monkey monkey.patch_all() def server(port): sock = socket.socket() sock.bind(('127.0.0.1', port)) sock.listen(500) while 1: conn, addr = sock.accept() #handle_request(conn) gevent.spawn(handle_request, conn) def handle_request(conn): try: while 1: data = conn.recv(1024) if not data: break print("recv:",data) conn.send(data) except Exception as ex: print(ex) finally: conn.close() if __name__ == '__main__': server(8001) |
以上程式碼可以自行多開幾個客戶端,然後執行看看,是不是很酷,無論客戶端輸入什麼,服務端都能實時接收到。