Python 併發
Python
1. 多進程
Unix/Linux操作系統提供了一個fork()
系統調用,它非常特殊。普通的函數調用,調用一次,返回一次,但是fork()
調用一次,返回兩次,因爲操作系統自動把當前進程(稱爲父進程)複製了一份(稱爲子進程),然後,分別在父進程和子進程內返回。
子進程永遠返回0
,而父進程返回子進程的ID。這樣做的理由是,一個父進程可以fork出很多子進程,所以,父進程要記下每個子進程的ID,而子進程只需要調用getppid()
就可以拿到父進程的ID。
Python的multiprocessing
模塊提供了一個Process
類來代表一個進程對象
開啓進程:
from multiprocessing import Process import os # 子進程要執行的代碼 def run_proc(name): print('Run child process %s (%s)...' % (name, os.getpid())) if __name__=='__main__': print('Parent process %s.' % os.getpid()) p = Process(target=run_proc, args=('test',)) print('Child process will start.') p.start() p.join() print('Child process end.')
join()
方法可以等待子進程結束後再繼續往下運行,通常用於進程間的同步。
進程池:
如果要啓動大量的子進程,可以用進程池的方式批量創建子進程
p = Pool(4) for i in range(5): p.apply_async(long_time_task, args=(i,)) print('Waiting for all subprocesses done...') p.close() p.join()
進程間通信:
Process
之間肯定是需要通信的,操作系統提供了很多機制來實現進程間的通信。Python的multiprocessing
模塊包裝了底層的機制,提供了Queue
、Pipes
等多種方式來交換數據。
q = Queue() q.put(value) value = q.get(True) pipe_main = Pipe(False) pipe_main[1].send(best_plan_t) best_gene = pipe_main[0].recv()
2. 多線程
調用:
Python的標準庫提供了兩個模塊:_thread
和threading
,_thread
是低級模塊,threading
是高級模塊,對_thread
進行了封裝。絕大多數情況下,我們只需要使用threading
這個高級模塊。
啓動一個線程就是把一個函數傳入並創建Thread
實例,然後調用start()
開始執行:
import time, threading # 新線程執行的代碼: def loop(): print('thread %s is running...' % threading.current_thread().name) n = 0 while n < 5: n = n + 1 print('thread %s >>> %s' % (threading.current_thread().name, n)) time.sleep(1) print('thread %s ended.' % threading.current_thread().name) print('thread %s is running...' % threading.current_thread().name) t = threading.Thread(target=loop, name='LoopThread') t.start() t.join() print('thread %s ended.' % threading.current_thread().name)
Lock
多線程和多進程最大的不同在於,多進程中,同一個變量,各自有一份拷貝存在於每個進程中,互不影響,而多線程中,所有變量都由所有線程共享,所以,任何一個變量都可以被任何一個線程修改,因此,線程之間共享數據最大的危險在於多個線程同時改一個變量,把內容給改亂了。
創建一個鎖就是通過threading.Lock()
來實現線程間同步
獲得鎖的線程用完後一定要釋放鎖,否則那些苦苦等待鎖的線程將永遠等待下去,成爲死線程。所以我們用try...finally
來確保鎖一定會被釋放。
鎖的好處就是確保了某段關鍵代碼只能由一個線程從頭到尾完整地執行,壞處當然也很多,首先是阻止了多線程併發執行,包含鎖的某段代碼實際上只能以單線程模式執行,效率就大大地下降了。其次,由於可以存在多個鎖,不同的線程持有不同的鎖,並試圖獲取對方持有的鎖時,可能會造成死鎖,導致多個線程全部掛起,既不能執行,也無法結束,只能靠操作系統強制終止。
因爲Python的線程雖然是真正的線程,但解釋器執行代碼時,有一個GIL鎖:Global Interpreter Lock,任何Python線程執行前,必須先獲得GIL鎖,然後,每執行100條字節碼,解釋器就自動釋放GIL鎖,讓別的線程有機會執行。這個GIL全局鎖實際上把所有線程的執行代碼都給上了鎖,所以,多線程在Python中只能交替執行,即使100個線程跑在100核CPU上,也只能用到1個核。
ThreadLocal:
ThreadLocal
解決了參數在一個線程中各個函數之間互相傳遞的問題。
全局變量local_school
就是一個ThreadLocal
對象,每個Thread
對它都可以讀寫student
屬性,但互不影響。你可以把local_school
看成全局變量,但每個屬性如local_school.student
都是線程的局部變量,可以任意讀寫而互不干擾,也不用管理鎖的問題,ThreadLocal
內部會處理。
import threading # 創建全局ThreadLocal對象: local_school = threading.local() def process_student(): # 獲取當前線程關聯的student: std = local_school.student print('Hello, %s (in %s)' % (std, threading.current_thread().name)) def process_thread(name): # 綁定ThreadLocal的student: local_school.student = name process_student() t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A') t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B') t1.start() t2.start() t1.join() t2.join()
3. 協程
Coroutine,又叫協程,是輕量級線程,擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。
協程的應用場景:I/O 密集型任務。這一點與多線程有些類似,但協程調用是在一個線程內進行的,是單線程,切換的開銷小,因此效率上略高於多線程。而且協程不需要鎖,而線程需要鎖來進行數據同步。協程是編譯器級的,Process和Thread是操作系統級的。
Process和Thread是os通過調度算法,保存當前的上下文,然後從上次暫停的地方再次開始計算,重新開始的地方不可預期,每次CPU計算的指令數量和代碼跑過的CPU時間是相關的,跑到os分配的cpu時間到達後就會被os強制掛起。Coroutine是編譯器的魔術,通過插入相關的代碼使得代碼段能夠實現分段式的執行,重新開始的地方是yield關鍵字指定的,一次一定會跑到一個yield對應的地方。
來看例子:
傳統的生產者-消費者模型是一個線程寫消息,一個線程取消息,通過鎖機制控制隊列和等待,但一不小心就可能死鎖。
如果改用協程,生產者生產消息後,直接通過yield
跳轉到消費者開始執行,待消費者執行完畢後,切換回生產者繼續生產,效率極高:
def consumer(): r = '' while True: n = yield r if not n: return print('[CONSUMER] Consuming %s...' % n) r = '200 OK' def produce(c): c.send(None) n = 0 while n < 5: n = n + 1 print('[PRODUCER] Producing %s...' % n) r = c.send(n) print('[PRODUCER] Consumer return: %s' % r) c.close() c = consumer() produce(c)
執行結果:
[PRODUCER] Producing 1... [CONSUMER] Consuming 1... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 2... [CONSUMER] Consuming 2... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 3... [CONSUMER] Consuming 3... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 4... [CONSUMER] Consuming 4... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 5... [CONSUMER] Consuming 5... [PRODUCER] Consumer return: 200 OK
注意到consumer
函數是一個generator
,把一個consumer
傳入produce
後:
-
首先調用
c.send(None)
啓動生成器; -
然後,一旦生產了東西,通過
c.send(n)
切換到consumer
執行; -
consumer
通過yield
拿到消息,處理,又通過yield
把結果傳回; -
produce
拿到consumer
處理的結果,繼續生產下一條消息; -
produce
決定不生產了,通過c.close()
關閉consumer
,整個過程結束。
整個流程無鎖,由一個線程執行,produce
和consumer
協作完成任務,所以稱爲“協程”,而非線程的搶佔式多任務。
最後套用Donald Knuth的一句話總結協程的特點:
“子程序就是協程的一種特例。”
asyncio
asyncio
是Python 3.4版本引入的標準庫,直接內置了對異步IO的支持。
asyncio
的編程模型就是一個消息循環。我們從asyncio
模塊中直接獲取一個EventLoop
的引用,然後把需要執行的協程扔到EventLoop
中執行,就實現了異步IO。
用asyncio
實現Hello world
代碼如下:
import asyncio @asyncio.coroutine def hello(): print("Hello world!") # 異步調用asyncio.sleep(1): r = yield from asyncio.sleep(1) print("Hello again!") # 獲取EventLoop: loop = asyncio.get_event_loop() # 執行coroutine loop.run_until_complete(hello()) loop.close()
@asyncio.coroutine
把一個generator標記爲coroutine類型,然後,我們就把這個coroutine
扔到EventLoop
中執行。
hello()
會首先打印出Hello world!
,然後,yield from
語法可以讓我們方便地調用另一個generator
。由於asyncio.sleep()
也是一個coroutine
,所以線程不會等待asyncio.sleep()
,而是直接中斷並執行下一個消息循環。當asyncio.sleep()
返回時,線程就可以從yield from
拿到返回值(此處是None
),然後接着執行下一行語句。
把asyncio.sleep(1)
看成是一個耗時1秒的IO操作,在此期間,主線程並未等待,而是去執行EventLoop
中其他可以執行的coroutine
了,因此可以實現併發執行。
我們用Task封裝兩個coroutine
試試:
import threading import asyncio @asyncio.coroutine def hello(): print('Hello world! (%s)' % threading.currentThread()) yield from asyncio.sleep(1) print('Hello again! (%s)' % threading.currentThread()) loop = asyncio.get_event_loop() tasks = [hello(), hello()] loop.run_until_complete(asyncio.wait(tasks)) loop.close()
觀察執行過程:
Hello world! (<_MainThread(MainThread, started 140735195337472)>) Hello world! (<_MainThread(MainThread, started 140735195337472)>) (暫停約1秒) Hello again! (<_MainThread(MainThread, started 140735195337472)>) Hello again! (<_MainThread(MainThread, started 140735195337472)>)
由打印的當前線程名稱可以看出,兩個coroutine
是由同一個線程併發執行的。
如果把asyncio.sleep()
換成真正的IO操作,則多個coroutine
就可以由一個線程併發執行。
我們用asyncio
的異步網絡連接來獲取sina、sohu和163的網站首頁:
import asyncio @asyncio.coroutine def wget(host): print('wget %s...' % host) connect = asyncio.open_connection(host, 80) reader, writer = yield from connect header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host writer.write(header.encode('utf-8')) yield from writer.drain() while True: line = yield from reader.readline() if line == b'\r\n': break print('%s header > %s' % (host, line.decode('utf-8').rstrip())) # Ignore the body, close the socket writer.close() loop = asyncio.get_event_loop() tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']] loop.run_until_complete(asyncio.wait(tasks)) loop.close()
執行結果如下:
wget www.sohu.com... wget www.sina.com.cn... wget www.163.com... (等待一段時間) (打印出sohu的header) www.sohu.com header > HTTP/1.1 200 OK www.sohu.com header > Content-Type: text/html ... (打印出sina的header) www.sina.com.cn header > HTTP/1.1 200 OK www.sina.com.cn header > Date: Wed, 20 May 2015 04:56:33 GMT ... (打印出163的header) www.163.com header > HTTP/1.0 302 Moved Temporarily www.163.com header > Server: Cdn Cache Server V2.0 ...
可見3個連接由一個線程通過coroutine
併發完成。
asyncio
提供了完善的異步IO支持;
異步操作需要在coroutine
中通過yield from
完成;
多個coroutine
可以封裝成一組Task然後併發執行。
async/await
用asyncio
提供的@asyncio.coroutine
可以把一個generator標記爲coroutine類型,然後在coroutine內部用yield from
調用另一個coroutine實現異步操作。
爲了簡化並更好地標識異步IO,從Python 3.5開始引入了新的語法async
和await
,可以讓coroutine的代碼更簡潔易讀。
請注意,async
和await
是針對coroutine的新語法,要使用新的語法,只需要做兩步簡單的替換:
-
把
@asyncio.coroutine
替換爲async
; -
把
yield from
替換爲await
。
讓我們對比一下上一節的代碼:
@asyncio.coroutine def hello(): print("Hello world!") r = yield from asyncio.sleep(1) print("Hello again!")
用新語法重新編寫如下:
async def hello(): print("Hello world!") r = await asyncio.sleep(1) print("Hello again!")
剩下的代碼保持不變。
參考:
https://www.liaoxuefeng.com 廖雪峯Python教程