序言
進程與線程概述:
很多同學都聽說過,現代作業系統比如Mac OS X,UNIX,Linux,Windows等,都是支援“多任務”的作業系統。
什麼叫“多任務”呢?簡單地說,就是作業系統可以同時運行多個任務。打個比方,你一邊在用瀏覽器上網,一邊在聽MP3,一邊在用Word趕作業,這就是多任務,至少同時有3個任務正在運行。還有很多任務悄悄地在後台同時運行著,只是案頭上沒有顯示而已。
現在,多核CPU已經非常普及了,但是,即使過去的單核CPU,也可以執行多任務。由於CPU執行代碼都是順序執行的,那麼,單核CPU是怎麼執行多任務的呢?
答案就是作業系統輪流讓各個任務交替執行,任務1執行0.01秒,切換到任務2,任務2執行0.01秒,再切換到任務3,執行0.01秒……這樣反覆執行下去。表面上看,每個任務都是交替執行的,但是,由於CPU的執行速度實在是太快了,我們感覺就像所有任務都在同時執行一樣。
真正的並行執行多任務只能在多核CPU上實現,但是,由於任務數量遠遠多於CPU的核心數量,所以,作業系統也會自動把很多任務輪流調度到每個核心上執行。
對於作業系統來說,一個任務就是一個進程(Process),比如開啟一個瀏覽器就是啟動一個瀏覽器進程,開啟一個記事本就啟動了一個記事本進程,開啟兩個記事本就啟動了兩個記事本進程,開啟一個Word就啟動了一個Word進程。
有些進程還不止同時幹一件事,比如Word,它可以同時進行打字、拼字檢查、列印等事情。在一個進程內部,要同時幹多件事,就需要同時運行多個“子任務”,我們把進程內的這些“子任務”稱為線程(Thread)。
由於每個進程至少要幹一件事,所以,一個進程至少有一個線程。當然,像Word這種複雜的進程可以有多個線程,多個線程可以同時執行,多線程的執行方式和多進程是一樣的,也是由作業系統在多個線程之間快速切換,讓每個線程都短暫地交替運行,看起來就像同時執行一樣。當然,真正地同時執行多線程需要多核CPU才可能實現。
我們前面編寫的所有的Python程式,都是執行單任務的進程,也就是只有一個線程。如果我們要同時執行多個任務怎麼辦?
有兩種解決方案:
一種是啟動多個進程,每個進程雖然只有一個線程,但多個進程可以一塊執行多個任務。
還有一種方法是啟動一個進程,在一個進程內啟動多個線程,這樣,多個線程也可以一塊執行多個任務。
當然還有第三種方法,就是啟動多個進程,每個進程再啟動多個線程,這樣同時執行的任務就更多了,當然這種模型更複雜,實際很少採用。
總結一下就是,多任務的實現有3種方式:
同時執行多個任務通常各個任務之間並不是沒有關聯的,而是需要相互連信和協調,有時,任務1必須暫停等待任務2完成後才能繼續執行,有時,任務3和任務4又不能同時執行,所以,多進程和多線程的程式的複雜度要遠遠高於我們前面寫的單進程單線程的程式。
因為複雜度高,調試困難,所以,不是迫不得已,我們也不想編寫多任務。但是,有很多時候,沒有多任務還真不行。想想在電腦上看電影,就必須由一個線程播放視頻,另一個線程播放音頻,否則,單線程實現的話就只能先把視頻播放完再播放音頻,或者先把音頻播放完再播放視頻,這顯然是不行的。
Python既支援多進程,又支援多線程,我們會討論如何編寫這兩種多任務程式。
進程
初識:
要讓Python程式實現多進程(multiprocessing),我們先瞭解作業系統的相關知識。Unix/Linux作業系統提供了一個fork()系統調用,它非常特殊。普通的函數調用,調用一次,返回一次,但是fork()調用一次,返回兩次,因為作業系統自動把當前進程(稱為父進程)複製了一份(稱為子進程),然後,分別在父進程和子進程內返回。子進程永遠返回0,而父進程返回子進程的ID。這樣做的理由是,一個父進程可以fork出很多子進程,所以,父進程要記下每個子進程的ID,而子進程只需要調用getppid()就可以拿到父進程的ID。Python的os模組封裝了常見的系統調用,其中就包括fork,可以在Python程式中輕鬆建立子進程:
import osprint('Process (%s) start...' % os.getpid())# Only works on Unix/Linux/Mac:pid = os.fork()if pid == 0: print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))else: print('I (%s) just created a child process (%s).' % (os.getpid(), pid)) # Process (44587) start...# I (44587) just created a child process (44588).# I am child process (44588) and my parent is 44587.
由於Windows沒有fork調用,上面的代碼在Windows上無法運行。由於Mac系統是基於BSD(Unix的一種)核心,所以,在Mac下運行是沒有問題的,推薦大家用Mac學Python!有了fork調用,一個進程在接到新任務時就可以複製出一個子進程來處理新任務,常見的Apache伺服器就是由父進程監聽連接埠,每當有新的http請求時,就fork出子進程來處理新的http請求。
multiprocessing模組:
如果你打算編寫多進程的服務程式,Unix/Linux無疑是正確的選擇。由於Windows沒有fork調用,難道在Windows上無法用Python編寫多進程的程式?由於Python是跨平台的,自然也應該提供一個跨平台的多進程支援。multiprocessing模組就是跨平台版本的多進程模組。multiprocessing模組提供了一個Process類來代表一個進程對象,下面的例子示範了啟動一個子進程並等待其結束:
import osimport time# 子進程要執行的代碼def run_proc(name): time.sleep(1) 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',)) # args裡面為何要用,隔開? p.start() # 子進程啟動,不加這個子進程不執行 p.join() # 等待子進程p的執行完畢後再向下執行,不加此項,主程式執行完畢,子進程依然會繼續執行不受影響 print('Child process end.'),# Parent process 8428.# Run child process test (9392)...# Child process end.
Process執行個體化時執行self._args = tuple(args)操作,如果不用,隔開產生的slef._args就是一個個字母了,傳入兩個參數以上是就不用加,號了,如下:
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None):assert group is None, 'group argument must be None for now'count = next(_process_counter) self._identity = _current_process._identity + (count,) self._config = _current_process._config.copy() self._parent_pid = os.getpid() self._popen = None self._target = target self._args = tuple(args)a =('ers')b = tuple(a)print(b)# ('e', 'r', 's')a1 =('ers','gte')b1 = tuple(a1)print(b1)# ('ers', 'gte')Process代碼
Pool進程池:
如果要啟動大量的子進程,可以用進程池的方式大量建立子進程:
from multiprocessing import Pool,cpu_countimport os, time, randomdef long_time_task(name): print('Run task %s (%s)...' % (name, os.getpid())) start = time.time() time.sleep(random.random() * 3) end = time.time() print('Task %s runs %0.2f seconds.' % (name, (end - start)))def Bar(arg): print('-->exec done:',arg,os.getpid())if __name__=='__main__': print('Parent process %s.' % os.getpid()) p = Pool(cpu_count()) # 擷取當前cpu核心數,多核cpu的情況下多進程才能實現真正的並發 for i in range(5): # p.apply_async(func=long_time_task, args=(i,), callback=Bar) #callback回調 執行完func後再執行callback 用主程式執行 p.apply_async(long_time_task, args=(i,)) print('Waiting for all subprocesses done...') p.close() p.join() # !等待進程池執行完畢,不然主進程執行完畢後,進程池直接關閉 print('All subprocesses done.')# Parent process 4492.# Waiting for all subprocesses done...# Run task 0 (3108)...# Run task 1 (7936)...# Run task 2 (11236)...# Run task 3 (8284)...# Task 2 runs 0.86 seconds.# Run task 4 (11236)...# Task 0 runs 1.34 seconds.# Task 1 runs 1.49 seconds.# Task 3 runs 2.62 seconds.# Task 4 runs 1.90 seconds.# All subprocesses done.
重點:另進程池裡的進程執行完畢後,進程關閉自動銷毀,不再佔用記憶體,同理,非進程池建立的子進程,執行完畢後也是自動銷毀,具體測試如下:
from multiprocessing import Pool,cpu_countimport os, time, randomdef long_time_task(name):print('Run task %s (%s)...' % (name, os.getpid())) start = time.time() time.sleep(random.random() * 3) end = time.time()print('Task %s runs %0.2f seconds.' % (name, (end - start)))def count_process():import psutil pids = psutil.pids() process_name = []for pid in pids: p = psutil.Process(pid) process_name.append(p.name()) # 擷取進程名# process_name.append(p.num_threads()) # 擷取進程的線程數# print process_nameprint len(process_name)if __name__=='__main__':print('Parent process %s.' % os.getpid()) p = Pool(4)for i in range(5): p.apply_async(long_time_task, args=(i,))print('Waiting for all subprocesses done...') count_process() # 進程池開始時進程數(包含系統其他應用進程) p.close() p.join() count_process() # 進程池關閉時進程數print('All subprocesses done.')# Parent process 8860.# Waiting for all subprocesses done...# Run task 0 (2156)...# Run task 1 (1992)...# Run task 2 (10680)...# Run task 3 (11216)...# 109 開始# Task 2 runs 0.93 seconds.# Run task 4 (10680)...# Task 1 runs 1.71 seconds.# Task 3 runs 2.01 seconds.# Task 0 runs 2.31 seconds.# Task 4 runs 2.79 seconds.# 105 結束# All subprocesses done.進程池子進程執行完畢後銷毀
代碼解讀:
對Pool對象調用join()方法會等待所有子進程執行完畢,調用join()之前必須先調用close(),調用close()之後就不能繼續添加新的Process了。
請注意輸出的結果,task 0,1,2,3是立刻執行的,而task 4要等待前面某個task完成後才執行,這是因為Pool的預設大小在我的電腦上是4,因此,最多同時執行4個進程。這是Pool有意設計的限制,並不是作業系統的限制。如果改成:
p = Pool(5)
就可以同時跑5個進程。
由於Pool的預設大小是CPU的核心數,如果你不幸擁有8核CPU,你要提交至少9個子進程才能看到上面的等待效果。
處理序間通訊:
Process之間肯定是需要通訊的,作業系統提供了很多機制來實現進程間的通訊。Python的multiprocessing模組封裝了底層的機制,提供了Queue、Pipes等多種方式來交換資料。
我們以Queue為例,在父進程中建立兩個子進程,一個往Queue裡寫資料,一個從Queue裡讀資料:
from multiprocessing import Process, Queueimport os, time, random# 寫資料進程執行的代碼: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())# 讀資料進程執行的代碼: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__': # 父進程建立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進程裡是死迴圈,無法等待其結束,只能強行終止: pr.terminate() # 強制關閉子進程# Process to write: 9472# Put A to queue...# Process to read: 3948# 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()的細節。由於Windows沒有fork調用,因此,multiprocessing需要“類比”出fork的效果,父進程所有Python對象都必須通過pickle序列化再傳到子進程去,所有,如果multiprocessing在Windows下調用失敗了,要先考慮是不是pickle失敗了。
進程間共用資料:
有時候我們不僅僅需要進程間資料轉送,也需要多進程間進行資料共用,即可以使用同一全域變數;如:為何下面程式的列表輸出為空白?
from multiprocessing import Process, Managerimport os# manager = Manager()vip_list = []#vip_list = manager.list()def testFunc(cc): vip_list.append(cc) print 'process id:', os.getpid()if __name__ == '__main__': threads = [] for ll in range(10): t = Process(target=testFunc, args=(ll,)) t.daemon = True threads.append(t) for i in range(len(threads)): threads[i].start() for j in range(len(threads)): threads[j].join() print "------------------------" print 'process id:', os.getpid() print vip_list# process id: 9436# process id: 11120# process id: 10636# process id: 1380# process id: 10976# process id: 10708# process id: 2524# process id: 9392# process id: 10060# process id: 8516# ------------------------# process id: 9836# []
如果你瞭解 python 的多執行緒模式,GIL 問題,然後瞭解多線程、多進程原理,上述問題不難回答,不過如果你不知道也沒關係,跑一下上面的代碼你就知道是什麼問題了。因為進程間記憶體是獨立的
正如上面提到的,在進行並發編程時,最好儘可能避免使用共用狀態。在使用多個進程時尤其如此。但是,如果您確實需要使用一些共用資料,那麼多處理提供了兩種方法。
① 共用記憶體:
資料可以使用值或數組儲存在共用記憶體映射中。例如,下面的代碼:
from multiprocessing import Process, Value, Arraydef f(n, a): n.value = 3.1415927 for i in range(len(a)): a[i] = -a[i]if __name__ == '__main__': num = Value('d', 0.0) arr = Array('i', range(10)) p = Process(target=f, args=(num, arr)) p.start() p.join() print num.value print arr[:] # 3.1415927# [0, -1, -2, -3, -4, -5, -6, -7, -8, -9]
在建立num和arr時使用的“i”和“i”參數是數組模組使用的類型的類型:“表示雙精確度浮點數”,“i”表示一個已簽名的整數。這些共用對象將是進程和安全執行緒的。為了更靈活地使用共用記憶體,您可以使用多處理。sharedctypes模組支援建立從共用記憶體中分配的任意類型的ctypes對象。
② 服務進程:
manager()返回的manager對象控制一個儲存Python對象的伺服器處理序,並允許其他進程使用代理來操作它們。manager()返回的管理器將支援類型列list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Queue, Value and Array。如下:
from multiprocessing import Process, Managerdef f(d, l): d[1] = '1' d['2'] = 2 d[0.25] = None l.reverse()if __name__ == '__main__': manager = Manager() d = manager.dict() l = manager.list(range(10)) p = Process(target=f, args=(d, l)) p.start() p.join() print d print l# {0.25: None, 1: '1', '2': 2}# [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
伺服器處理序管理器比使用共用記憶體對象更靈活,因為它們可以用來支援任意物件類型。另外,單個管理器可以通過網路上不同電腦上的進程共用。但是,它們比使用共用記憶體要慢。
更多-》》點擊
小結
在Unix/Linux下,可以使用fork()調用實現多進程。
要實現跨平台的多進程,可以使用multiprocessing模組。
處理序間通訊是通過Queue(多進程間)、Pipes(兩個進程間)等實現的。
補充小知識點-》父進程開闢子進程,子進程開闢子子進程,如果把子進程殺掉,子子進程會被殺死嗎?
import timefrom multiprocessing import Processimport osdef count_process():import psutil pids = psutil.pids()print len(pids)def test3(): count_process()for i in range(10):print "test3 %s"%os.getpid() time.sleep(0.5)def test1():print "test1 %s"%os.getpid() p2 = Process(target=test3, name="protest2") p2.start() p2.join()if __name__ == '__main__': count_process() p1 = Process(target=test1, name="protest1") p1.start() time.sleep(2) p1.terminate() time.sleep(2) count_process()for i in range(10):print(i) time.sleep(1)# 86# test1 9500# 88# test3 3964# test3 3964# test3 3964# test3 3964# test3 3964# test3 3964# test3 3964# test3 3964# 87# 0# test3 3964# test3 3964# 1# 2# 3# 4# 5# 6# 7# 8# 9
子子進程的心路曆程