1.多進程與多線程介紹 / 區(qū)別
- 現(xiàn)在,多核CPU已經(jīng)非常普及了,但是,即使過去的單核CPU,也可以執(zhí)行多任務。由于CPU執(zhí)行代碼都是順序執(zhí)行的,那么,單核CPU是怎么執(zhí)行多任務的呢?
- 答案就是操作系統(tǒng)輪流讓各個任務交替執(zhí)行,任務1執(zhí)行0.01秒,切換到任務2,任務2執(zhí)行0.01秒,再切換到任務3,執(zhí)行0.01秒……這樣反復執(zhí)行下去。表面上看,每個任務都是交替執(zhí)行的,但是,由于CPU的執(zhí)行速度實在是太快了,我們感覺就像所有任務都在同時執(zhí)行一樣。
- 真正的并行執(zhí)行多任務只能在多核CPU上實現(xiàn),但是,由于任務數(shù)量遠遠多于CPU的核心數(shù)量,所以,操作系統(tǒng)也會自動把很多任務輪流調度到每個核心上執(zhí)行。
我們前面編寫的所有的Python程序,都是執(zhí)行單任務的進程,也就是只有一個線程。如果我們要同時執(zhí)行多個任務怎么辦?
有三種解決方案:
一種是啟動多個進程,每個進程雖然只有一個線程,但多個進程可以一塊執(zhí)行多個任務。
還有一種方法是啟動一個進程,在一個進程內(nèi)啟動多個線程,這樣,多個線程也可以一塊執(zhí)行多個任務。
第三種方法,就是啟動多個進程,每個進程再啟動多個線程,這樣同時執(zhí)行的任務就更多了,當然這種模型更復雜,實際很少采用。
總結一下就是,多任務的實現(xiàn)有3種方式:
- 多進程模式;
- 多線程模式;
- 多進程+多線程模式。
同時執(zhí)行多個任務通常各個任務之間并不是沒有關聯(lián)的,而是需要相互通信和協(xié)調,有時,任務1必須暫停等待任務2完成后才能繼續(xù)執(zhí)行,有時,任務3和任務4又不能同時執(zhí)行,所以,多進程和多線程的程序的復雜度要遠遠高于我們前面寫的單進程單線程的程序。
因為復雜度高,調試困難,所以,不是迫不得已,我們也不想編寫多任務。但是,有很多時候,沒有多任務還真不行。想想在電腦上看電影,就必須由一個線程播放視頻,另一個線程播放音頻,否則,單線程實現(xiàn)的話就只能先把視頻播放完再播放音頻,或者先把音頻播放完再播放視頻,這顯然是不行的。
多線程和多進程最大的不同在于,多進程中,同一個變量,各自有一份拷貝存在于每個進程中,互不影響,而多線程中,所有變量都由所有線程共享,所以,任何一個變量都可以被任何一個線程修改,因此,線程之間共享數(shù)據(jù)最大的危險在于多個線程同時改一個變量,把內(nèi)容給改亂了。

- 線程是最小的執(zhí)行單元,而進程由至少一個線程組成。如何調度進程和線程,完全由操作系統(tǒng)決定,程序自己不能決定什么時候執(zhí)行,執(zhí)行多長時間。
- 如果要多線程工作,有個比喻就是首先要單身,然后放棄社交,哈哈,是不是有點形象呢?
- Python既支持多進程,又支持多線程,我們會討論如何編寫這兩種多任務程序。
2.多進程
Linux/Unix下實現(xiàn)多進程
Unix/Linux操作系統(tǒng)提供了一個
fork()系統(tǒng)調用,它非常特殊。普通的函數(shù)調用,調用一次,返回一次,但是fork()調用一次,返回兩次,因為操作系統(tǒng)自動把當前進程(稱為父進程)復制了一份(稱為子進程),然后,分別在父進程和子進程內(nèi)返回。
子進程永遠返回0,而父進程返回子進程的ID。這樣做的理由是,一個父進程可以fork出很多子進程,所以,父進程要記下每個子進程的ID,而子進程只需要調用getppid()就可以拿到父進程的ID。
Python的os模塊封裝了常見的系統(tǒng)調用,其中就包括fork,可以在Python程序中輕松創(chuàng)建子進程:
import os
print('當前進程:%s 啟動中 ....' % os.getpid())
pid = os.fork()
if pid == 0:
print('子進程:%s,父進程是:%s' % (os.getpid(), os.getppid()))
else:
print('進程:%s 創(chuàng)建了子進程:%s' % (os.getpid(),pid )) # pid為父進程返回的自進程ID
輸出結果:
當前進程:27223 啟動中 ....
進程:27223 創(chuàng)建了子進程:27224
子進程:27224,父進程是:27223
Windows下實現(xiàn)多進程
如果你打算編寫多進程的服務程序,Unix/Linux無疑是正確的選擇。由于Windows沒有
fork調用,難道在Windows上無法用Python編寫多進程的程序?
由于Python是跨平臺的,自然也應該提供一個跨平臺的多進程支持。multiprocessing模塊就是跨平臺版本的多進程模塊。multiprocessing模塊提供了一個Process類來代表一個進程對象,下面的例子演示了啟動一個子進程并等待其結束:
from multiprocessing import Process
import os
# 子進程要執(zhí)行的代碼
def run_proc(name):
print('運行子進程 %s (%s)...' % (name, os.getpid()))
print('子進程:%s,父進程:%s' %(os.getpid(), os.getppid()))
if __name__=='__main__':
print('父進程 %s.' % os.getpid())
p = Process(target=run_proc, args=('test',)) # 注意args中的逗號
print('子進程將開始.')
p.start()
p.join()
print('子進程結束.')
運行結果:
父進程 9344.
子進程將開始.
運行子進程 test (8504)...
子進程:8504,父進程:9344
子進程結束.
創(chuàng)建子進程時,只需要傳入一個執(zhí)行函數(shù)和函數(shù)的參數(shù),創(chuàng)建一個
Process實例,用start()方法啟動,這樣創(chuàng)建進程比fork()還要簡單。
join()方法可以等待子進程結束后再繼續(xù)往下運行,通常用于進程間的同步。
創(chuàng)建Pool進程池
from multiprocessing import Pool
import os, time, random
def long_time_task(name):
print('運行任務 %s (%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('任務 %s 運行了 %0.2f 秒.' % (name, (end - start)))
if __name__=='__main__':
print('父進程 %s.' % os.getpid())
p = Pool(4)
for i in range(5):
p.apply_async(long_time_task, args=(i,))
print('等待所有子進程完成...')
p.close()
p.join()
print('所有子進程已完成.')
運行結果:
父進程 11488.
等待所有子進程完成...
運行任務 0 (8520)...
運行任務 1 (8936)...
運行任務 2 (2588)...
運行任務 3 (2136)...
任務 2 運行了 1.02 秒.
運行任務 4 (2588)...
任務 4 運行了 0.20 秒.
任務 0 運行了 1.85 秒.
任務 1 運行了 2.04 秒.
任務 3 運行了 2.96 秒.
所有子進程已完成.
代碼解讀:
對Pool對象調用join()方法會等待所有子進程執(zhí)行完畢,調用join()之前必須先調用close(),調用close()之后就不能繼續(xù)添加新的Process了。
請注意輸出的結果,task 0,1,2,3是立刻執(zhí)行的,而task 4要等待前面某個task完成后才執(zhí)行,這是因為Pool的默認大小在我的電腦上是4,因此,最多同時執(zhí)行4個進程。這是Pool有意設計的限制,并不是操作系統(tǒng)的限制。
如果改成:p = Pool(5)就可以同時跑5個進程。
由于Pool的默認大小是CPU的核數(shù),如果你不幸擁有8核CPU,你要提交至少9個子進程才能看到上面的等待效果。
進程間通信
Process之間肯定是需要通信的,操作系統(tǒng)提供了很多機制來實現(xiàn)進程間的通信。Python的multiprocessing模塊包裝了底層的機制,提供了Queue、Pipes等多種方式來交換數(shù)據(jù)。
我們以Queue為例,在父進程中創(chuàng)建兩個子進程,一個往Queue里寫數(shù)據(jù),一個從Queue里讀數(shù)據(jù):
from multiprocessing import Process, Queue
import os, time, random
# 寫數(shù)據(jù)進程執(zhí)行的代碼:
def write(q):
print('Process to write: %s' % os.getpid())
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
q.put(value)
time.sleep(random.random())
# 讀數(shù)據(jù)進程執(zhí)行的代碼:
def read(q):
print('Process to read: %s' % os.getpid())
while True:
value = q.get(True)
print('Get %s from queue.' % value)
if __name__=='__main__':
# 父進程創(chuàng)建Queue,并傳給各個子進程:
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 啟動子進程pw,寫入:
pw.start()
# 啟動子進程pr,讀取:
pr.start()
# 等待pw結束:
pw.join()
# pr進程里是死循環(huán),無法等待其結束,只能強行終止:
pr.terminate()
運行結果:
Process to write: 11560
Put A to queue...
Process to read: 10976
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.
在Unix/Linux下,
multiprocessing模塊封裝了fork()調用,使我們不需要關注fork()的細節(jié)。由于Windows沒有fork調用,因此,multiprocessing需要“模擬”出fork的效果,父進程所有Python對象都必須通過pickle序列化再傳到子進程去,所有,如果multiprocessing在Windows下調用失敗了,要先考慮是不是pickle失敗了。
3.多線程
由于線程是操作系統(tǒng)直接支持的執(zhí)行單元,因此,高級語言通常都內(nèi)置多線程的支持,Python也不例外,并且,Python的線程是真正的Posix Thread,而不是模擬出來的線程。
Python的標準庫提供了兩個模塊:_thread和threading,_thread是低級模塊,threading是高級模塊,對_thread進行了封裝。絕大多數(shù)情況下,我們只需要使用threading這個高級模塊。
啟動一個線程就是把一個函數(shù)傳入并創(chuàng)建Thread實例,然后調用start()開始執(zhí)行:
下面來寫一個多線程的死循環(huán)
import threading, multiprocessing
def loop():
x = 0
while True:
x = x ^ 1
for i in range(multiprocessing.cpu_count()):
t = threading.Thread(target=loop)
t.start()
python使用多線程只能用到CPU的一個核心,因為Python的線程雖然是真正的線程,但解釋器執(zhí)行代碼時,有一個GIL鎖:Global Interpreter Lock,任何Python線程執(zhí)行前,必須先獲得GIL鎖,然后,每執(zhí)行100條字節(jié)碼,解釋器就自動釋放GIL鎖,讓別的線程有機會執(zhí)行。這個GIL全局鎖實際上把所有線程的執(zhí)行代碼都給上了鎖,所以,多線程在Python中只能交替執(zhí)行,即使100個線程跑在100核CPU上,也只能用到1個核。所以,在Python中,可以使用多線程,但不要指望能有效利用多核。
筆記摘自廖雪峰的官方網(wǎng)站