原文地址:https://tracholar.github.io/wiki/python/python-multiprocessing-tutorial.html
簡介
早已進入多核時代的計算機,怎能不用多線程和多進程進行加速。
我在使用python的過程中,用到過幾次多線程和多進程加速,覺得
充分利用CPU節省時間是一種很有“延長生命”的感覺。現將網絡上看到的python的
多線程和多進程編程常用的知識點彙總在這裏。
線程與進程
線程與進程是操作系統裏面的術語,簡單來講,每一個應用程序都有一個自己的進程。
操作系統會爲這些進程分配一些執行資源,例如內存空間等。
在進程中,又可以創建一些線程,他們共享這些內存空間,並由操作系統調用,
以便並行計算。
32位系統受限於總線寬度,單個進程最多能夠訪問的地址空間
只有4G,利用物理地址擴展(PAE)
技術,可以讓CPU訪問超過4G內存。但是在單個進程還是隻能訪問4G
空間,PAE的優勢是可以讓不同進程累計使用的內存超過4G。
在個人電腦上,還是建議使用64位系統,便於使用大內存
提升程序的運行性能。
多線程編程
線程的狀態
創建線程之後,線程並不是始終保持一個狀態。其狀態大概如下:
- New 創建。
- Runnable 就緒。等待調度
- Running 運行。
- Blocked 阻塞。阻塞可能在 Wait Locked Sleeping
- Dead 消亡
線程的類型
線程有着不同的狀態,也有不同的類型。大致可分爲:
- 主線程
- 子線程
- 守護線程(後臺線程)
- 前臺線程
python的GIL
GIL即全局解釋器鎖,它使得python的多線程無法充分利用
多核的優勢,但是對於I/O操作頻繁的爬蟲之類的程序,
利用多線程帶來的優勢還是很明顯的。
如果要利用多核優勢,還是用多進程吧。
創建線程
Python提供兩個模塊進行多線程的操作,分別是thread
和threading
,
前者是比較低級的模塊,用於更底層的操作,一般應用級別的開發不常用。
第一種方法是創建threading.Thread
的子類,重寫run
方法。
import time import threading class MyThread(threading.Thread): def run(self): for i in range(5): print 'thread {}, @number: {}'.format(self.name, i) time.sleep(1) def main(): print "Start main threading" # 創建三個線程 threads = [MyThread() for i in range(3)] # 啓動三個線程 for t in threads: t.start() print "End Main threading" if __name__ == '__main__': main()
輸入如下:(不同的環境不一樣)
Start main threading thread Thread-1, @number: 0 thread Thread-2, @number: 0 thread Thread-3, @number: 0 End Main threading thread Thread-1, @number: 1 thread Thread-3, @number: 1 thread Thread-2, @number: 1 thread Thread-3, @number: 2 thread Thread-1, @number: 2 thread Thread-2, @number: 2 thread Thread-2, @number: 3 thread Thread-1, @number: 3 thread Thread-3, @number: 3
線程合併(join方法)
主線程結束後,子線程還在運行,join
方法使得主線程等到子線程結束
時才退出。
def main(): print "Start main threading" threads = [MyThread() for i in range(3)] for t in threads: t.start() # 一次讓新創建的線程執行 join for t in threads: t.join() print "End Main threading"
線程同步與互斥鎖
爲了避免線程不同步造成是數據不同步,可以對資源進行加鎖。
也就是訪問資源的線程需要獲得鎖,才能訪問。
threading
模塊正好提供了一個Lock功能
mutex = threading.Lock()
在線程中獲取鎖
mutex.acquire()
使用完後,釋放鎖
mutex.release()
可重入鎖
爲了支持在同一線程中多次請求同一資源,
python提供了可重入鎖(RLock)。
RLock內部維護着一個Lock
和一個counter
變量,
counter
記錄了acquire的次數,從而使得資源可以被多次require。
直到一個線程所有的acquire都被release,其他的線程才能獲得資源。
創建RLock
mutex = threading.RLock()
線程內多次進入鎖和釋放鎖
class MyThread(threading.Thread): def run(self): if mutex.acquire(1): print "thread {} get mutex".format(self.name) time.sleep(1) mutex.acquire() mutex.release() mutex.release()
條件變量
實用鎖可以達到線程同步,前面的互斥鎖就是這種機制。更復雜的環境,需要針對鎖進行一些條件判斷。Python提供了Condition對象。它除了具有acquire和release方法之外,還提供了wait和notify方法。線程首先acquire一個條件變量鎖。如果條件不足,則該線程wait,如果滿足就執行線程,甚至可以notify其他線程。其他處於wait狀態的線程接到通知後會重新判斷條件。
條件變量可以看成不同的線程先後acquire獲得鎖,如果不滿足條件,可以理解爲被扔到一個(Lock或RLock)的waiting池。直達其他線程notify之後再重新判斷條件。該模式常用於生成消費者模式:
queue = [] con = threading.Condition() class Producer(threading.Thread): def run(self): while True: if con.acquire(): if len(queue) > 100: con.wait() else: elem = random.randrange(100) queue.append(elem) print "Producer a elem {}, Now size is {}".format(elem, len(queue)) time.sleep(random.random()) con.notify() con.release() class Consumer(threading.Thread): def run(self): while True: if con.acquire(): if len(queue) < 0: con.wait() else: elem = queue.pop() print "Consumer a elem {}. Now size is {}".format(elem, len(queue)) time.sleep(random.random()) con.notify() con.release() def main(): for i in range(3): Producer().start() for i in range(2): Consumer().start()
隊列
帶鎖的隊列Queue
。
創建10個元素的隊列
queue = Queue.Queue(10)
隊列通過put
加入元素,通過get
方法獲取元素。
線程通信
線程可以讀取共享的內存,通過內存做一些數據處理。
這就是線程通信的一種,python還提供了更加高級的線程通信接口。
Event對象可以用來進行線程通信,調用event對象的wait方法,
線程則會阻塞等待,直到別的線程set之後,纔會被喚醒。
class MyThread(threading.Thread): def __init__(self, event): super(MyThread, self).__init__() self.event = event def run(self): print "thread {} is ready ".format(self.name) self.event.wait() print "thread {} run".format(self.name) signal = threading.Event() def main(): start = time.time() for i in range(3): t = MyThread(signal) t.start() time.sleep(3) print "after {}s".format(time.time() - start) signal.set()
後臺線程
默認情況下,主線程退出之後,即使子線程沒有join。那麼主線程結束後,
子線程也依然會繼續執行。如果希望主線程退出後,
其子線程也退出而不再執行,則需要設置子線程爲後臺線程。python提供了setDeamon
方法。
進程
python中的多線程其實並不是真正的多線程,如果想要充分地使用多核CPU的資源,在python中大部分情況需要使用多進程。Python提供了非常好用的多進程包multiprocessing,只需要定義一個函數,Python會完成其他所有事情。藉助這個包,可以輕鬆完成從單進程到併發執行的轉換。multiprocessing支持子進程、通信和共享數據、執行不同形式的同步,提供了Process、Queue、Pipe、Lock等組件。
類Process
-
創建進程的類:
Process([group [, target [, name [, args [, kwargs]]]]])
,
target表示調用對象,args表示調用對象的位置參數元組。
kwargs表示調用對象的字典。name爲別名。group實質上不使用。 -
方法:is_alive()、join([timeout])、run()、start()、terminate()。其中,Process以start()啓動某個進程。
-
屬性:authkey、daemon(要通過start()設置)、exitcode(進程在運行時爲None、如果爲–N,表示被信號N結束)、name、pid。其中daemon是父進程終止後自動終止,且自己不能產生新進程,必須在start()之前設置。
例:創建函數並將其作爲單個進程
import multiprocessing import time def worker(interval): n = 5 while n > 0: print("The time is {0}".format(time.ctime())) time.sleep(interval) n -= 1 if __name__ == "__main__": p = multiprocessing.Process(target = worker, args = (3,)) p.start() print "p.pid:", p.pid print "p.name:", p.name print "p.is_alive:", p.is_alive()
結果
p.pid: 8736 p.name: Process-1 p.is_alive: True The time is Tue Apr 21 20:55:12 2015 The time is Tue Apr 21 20:55:15 2015 The time is Tue Apr 21 20:55:18 2015 The time is Tue Apr 21 20:55:21 2015 The time is Tue Apr 21 20:55:24 2015
例:創建函數並將其作爲多個進程
import multiprocessing import time def worker_1(interval): print "worker_1" time.sleep(interval) print "end worker_1" def worker_2(interval): print "worker_2" time.sleep(interval) print "end worker_2" def worker_3(interval): print "worker_3" time.sleep(interval) print "end worker_3" if __name__ == "__main__": p1 = multiprocessing.Process(target = worker_1, args = (2,)) p2 = multiprocessing.Process(target = worker_2, args = (3,)) p3 = multiprocessing.Process(target = worker_3, args = (4,)) p1.start() p2.start() p3.start() print("The number of CPU is:" + str(multiprocessing.cpu_count())) for p in multiprocessing.active_children(): print("child p.name:" + p.name + "\tp.id" + str(p.pid)) print "END!!!!!!!!!!!!!!!!!"
結果
The number of CPU is:4 child p.name:Process-3 p.id7992 child p.name:Process-2 p.id4204 child p.name:Process-1 p.id6380 END!!!!!!!!!!!!!!!!! worker_1 worker_3 worker_2 end worker_1 end worker_2 end worker_3
例:將進程定義爲類
import multiprocessing import time class ClockProcess(multiprocessing.Process): def __init__(self, interval): multiprocessing.Process.__init__(self) self.interval = interval def run(self): n = 5 while n > 0: print("the time is {0}".format(time.ctime())) time.sleep(self.interval) n -= 1 if __name__ == '__main__': p = ClockProcess(3) p.start()
注:進程p調用start()時,自動調用run()
結果
the time is Tue Apr 21 20:31:30 2015 the time is Tue Apr 21 20:31:33 2015 the time is Tue Apr 21 20:31:36 2015 the time is Tue Apr 21 20:31:39 2015 the time is Tue Apr 21 20:31:42 2015
例:daemon程序對比結果
不加daemon屬性
import multiprocessing import time def worker(interval): print("work start:{0}".format(time.ctime())); time.sleep(interval) print("work end:{0}".format(time.ctime())); if __name__ == "__main__": p = multiprocessing.Process(target = worker, args = (3,)) p.start() print "end!"
結果
end! work start:Tue Apr 21 21:29:10 2015 work end:Tue Apr 21 21:29:13 2015
加上daemon屬性
import multiprocessing import time def worker(interval): print("work start:{0}".format(time.ctime())); time.sleep(interval) print("work end:{0}".format(time.ctime())); if __name__ == "__main__": p = multiprocessing.Process(target = worker, args = (3,)) p.daemon = True p.start() print "end!"
結果
end!
注:因子進程設置了daemon屬性,主進程結束,它們就隨着結束了。
設置daemon執行完結束的方法
import multiprocessing import time def worker(interval): print("work start:{0}".format(time.ctime())); time.sleep(interval) print("work end:{0}".format(time.ctime())); if __name__ == "__main__": p = multiprocessing.Process(target = worker, args = (3,)) p.daemon = True p.start() p.join() print "end!"
結果
work start:Tue Apr 21 22:16:32 2015 work end:Tue Apr 21 22:16:35 2015 end!
Lock
當多個進程需要訪問共享資源的時候,Lock可以用來避免訪問的衝突。
import multiprocessing import sys def worker_with(lock, f): with lock: fs = open(f, 'a+') n = 10 while n > 1: fs.write("Lockd acquired via with\n") n -= 1 fs.close() def worker_no_with(lock, f): lock.acquire() try: fs = open(f, 'a+') n = 10 while n > 1: fs.write("Lock acquired directly\n") n -= 1 fs.close() finally: lock.release() if __name__ == "__main__": lock = multiprocessing.Lock() f = "file.txt" w = multiprocessing.Process(target = worker_with, args=(lock, f)) nw = multiprocessing.Process(target = worker_no_with, args=(lock, f)) w.start() nw.start() print "end"
結果(輸出文件)
Lockd acquired via with Lockd acquired via with Lockd acquired via with Lockd acquired via with Lockd acquired via with Lockd acquired via with Lockd acquired via with Lockd acquired via with Lockd acquired via with Lock acquired directly Lock acquired directly Lock acquired directly Lock acquired directly Lock acquired directly Lock acquired directly Lock acquired directly Lock acquired directly Lock acquired directly
Semaphore
Semaphore用來控制對共享資源的訪問數量,例如池的最大連接數。
import multiprocessing import time def worker(s, i): s.acquire() print(multiprocessing.current_process().name + "acquire"); time.sleep(i) print(multiprocessing.current_process().name + "release\n"); s.release() if __name__ == "__main__": s = multiprocessing.Semaphore(2) for i in range(5): p = multiprocessing.Process(target = worker, args=(s, i*2)) p.start()
結果
Process-1acquire Process-1release Process-2acquire Process-3acquire Process-2release Process-5acquire Process-3release Process-4acquire Process-5release Process-4release
Event
Event用來實現進程間同步通信。
import multiprocessing import time def wait_for_event(e): print("wait_for_event: starting") e.wait() print("wairt_for_event: e.is_set()->" + str(e.is_set())) def wait_for_event_timeout(e, t): print("wait_for_event_timeout:starting") e.wait(t) print("wait_for_event_timeout:e.is_set->" + str(e.is_set())) if __name__ == "__main__": e = multiprocessing.Event() w1 = multiprocessing.Process(name = "block", target = wait_for_event, args = (e,)) w2 = multiprocessing.Process(name = "non-block", target = wait_for_event_timeout, args = (e, 2)) w1.start() w2.start() time.sleep(3) e.set() print("main: event is set")
結果
wait_for_event: starting wait_for_event_timeout:starting wait_for_event_timeout:e.is_set->False main: event is set wairt_for_event: e.is_set()->True
Queue
Queue是多進程安全的隊列,可以使用Queue實現多進程之間的數據傳遞。put方法用以插入數據到隊列中,put方法還有兩個可選參數:blocked和timeout。如果blocked爲True(默認值),並且timeout爲正值,該方法會阻塞timeout指定的時間,直到該隊列有剩餘的空間。如果超時,會拋出Queue.Full異常。如果blocked爲False,但該Queue已滿,會立即拋出Queue.Full異常。
get方法可以從隊列讀取並且刪除一個元素。同樣,get方法有兩個可選參數:blocked和timeout。如果blocked爲True(默認值),並且timeout爲正值,那麼在等待時間內沒有取到任何元素,會拋出Queue.Empty異常。如果blocked爲False,有兩種情況存在,如果Queue有一個值可用,則立即返回該值,否則,如果隊列爲空,則立即拋出Queue.Empty異常。Queue的一段示例代碼:
import multiprocessing def writer_proc(q): try: q.put(1, block = False) except: pass def reader_proc(q): try: print q.get(block = False) except: pass if __name__ == "__main__": q = multiprocessing.Queue() writer = multiprocessing.Process(target=writer_proc, args=(q,)) writer.start() reader = multiprocessing.Process(target=reader_proc, args=(q,)) reader.start() reader.join() writer.join()
結果
1
Pipe
Pipe方法返回(conn1, conn2)代表一個管道的兩個端。Pipe方法有duplex參數,如果duplex參數爲True(默認值),那麼這個管道是全雙工模式,也就是說conn1和conn2均可收發。duplex爲False,conn1只負責接受消息,conn2只負責發送消息。
send和recv方法分別是發送和接受消息的方法。例如,在全雙工模式下,可以調用conn1.send發送消息,conn1.recv接收消息。如果沒有消息可接收,recv方法會一直阻塞。如果管道已經被關閉,那麼recv方法會拋出EOFError。
import multiprocessing import time def proc1(pipe): while True: for i in xrange(10000): print "send: %s" %(i) pipe.send(i) time.sleep(1) def proc2(pipe): while True: print "proc2 rev:", pipe.recv() time.sleep(1) def proc3(pipe): while True: print "PROC3 rev:", pipe.recv() time.sleep(1) if __name__ == "__main__": pipe = multiprocessing.Pipe() p1 = multiprocessing.Process(target=proc1, args=(pipe[0],)) p2 = multiprocessing.Process(target=proc2, args=(pipe[1],)) #p3 = multiprocessing.Process(target=proc3, args=(pipe[1],)) p1.start() p2.start() #p3.start() p1.join() p2.join() #p3.join()
結果
Pool
在利用Python進行系統管理的時候,特別是同時操作多個文件目錄,或者遠程控制多臺主機,並行操作可以節約大量的時間。當被操作對象數目不大時,可以直接利用multiprocessing中的Process動態成生多個進程,十幾個還好,但如果是上百個,上千個目標,手動的去限制進程數量卻又太過繁瑣,此時可以發揮進程池的功效。Pool可以提供指定數量的進程,供用戶調用,當有新的請求提交到pool中時,如果池還沒有滿,那麼就會創建一個新的進程用來執行該請求;但如果池中的進程數已經達到規定最大值,那麼該請求就會等待,直到池中有進程結束,纔會創建新的進程來它。
例:使用進程池
#coding: utf-8 import multiprocessing import time def func(msg): print "msg:", msg time.sleep(3) print "end" if __name__ == "__main__": pool = multiprocessing.Pool(processes = 3) for i in xrange(4): msg = "hello %d" %(i) pool.apply_async(func, (msg, )) #維持執行的進程總數爲processes,當一個進程執行完畢後會添加新的進程進去 print "Mark~ Mark~ Mark~~~~~~~~~~~~~~~~~~~~~~" pool.close() pool.join() #調用join之前,先調用close函數,否則會出錯。執行完close後不會有新的進程加入到pool,join函數等待所有子進程結束 print "Sub-process(es) done."
一次執行結果
mMsg: hark~ Mark~ Mark~~~~~~~~~~~~~~~~~~~~~~ello 0 msg: hello 1 msg: hello 2 end msg: hello 3 end end end Sub-process(es) done.
函數解釋:
apply_async(func[, args[, kwds[, callback]]]) 它是非阻塞,apply(func[, args[, kwds]])是阻塞的(理解區別,看例1例2結果區別) close() 關閉pool,使其不在接受新的任務。 terminate() 結束工作進程,不在處理未完成的任務。 join() 主進程阻塞,等待子進程的退出, join方法要在close或terminate之後使用。
執行說明:創建一個進程池pool,並設定進程的數量爲3,xrange(4)會相繼產生四個對象[0, 1, 2, 4],四個對象被提交到pool中,因pool指定進程數爲3,所以0、1、2會直接送到進程中執行,當其中一個執行完事後才空出一個進程處理對象3,所以會出現輸出“msg: hello 3”出現在”end”後。因爲爲非阻塞,主函數會自己執行自個的,不搭理進程的執行,所以運行完for循環後直接輸出“mMsg: hark~ Mark~ Mark~~~~~~~~~~~~~~~~~~~~~~”,主程序在pool.join()處等待各個進程的結束。
例:使用進程池(阻塞)
#coding: utf-8 import multiprocessing import time def func(msg): print "msg:", msg time.sleep(3) print "end" if __name__ == "__main__": pool = multiprocessing.Pool(processes = 3) for i in xrange(4): msg = "hello %d" %(i) pool.apply(func, (msg, )) #維持執行的進程總數爲processes,當一個進程執行完畢後會添加新的進程進去 print "Mark~ Mark~ Mark~~~~~~~~~~~~~~~~~~~~~~" pool.close() pool.join() #調用join之前,先調用close函數,否則會出錯。執行完close後不會有新的進程加入到pool,join函數等待所有子進程結束 print "Sub-process(es) done."
一次執行的結果
msg: hello 0 end msg: hello 1 end msg: hello 2 end msg: hello 3 end Mark~ Mark~ Mark~~~~~~~~~~~~~~~~~~~~~~ Sub-process(es) done.
例:使用進程池,並關注結果
msg: hello 0 msg: hello 1 msg: hello 2 end end end ::: donehello 0 ::: donehello 1 ::: donehello 2 Sub-process(es) done.
例:使用多個進程池
#coding: utf-8 import multiprocessing import os, time, random def Lee(): print "\nRun task Lee-%s" %(os.getpid()) #os.getpid()獲取當前的進程的ID start = time.time() time.sleep(random.random() * 10) #random.random()隨機生成0-1之間的小數 end = time.time() print 'Task Lee, runs %0.2f seconds.' %(end - start) def Marlon(): print "\nRun task Marlon-%s" %(os.getpid()) start = time.time() time.sleep(random.random() * 40) end=time.time() print 'Task Marlon runs %0.2f seconds.' %(end - start) def Allen(): print "\nRun task Allen-%s" %(os.getpid()) start = time.time() time.sleep(random.random() * 30) end = time.time() print 'Task Allen runs %0.2f seconds.' %(end - start) def Frank(): print "\nRun task Frank-%s" %(os.getpid()) start = time.time() time.sleep(random.random() * 20) end = time.time() print 'Task Frank runs %0.2f seconds.' %(end - start) if __name__=='__main__': function_list= [Lee, Marlon, Allen, Frank] print "parent process %s" %(os.getpid()) pool=multiprocessing.Pool(4) for func in function_list: pool.apply_async(func) #Pool執行函數,apply執行函數,當有一個進程執行完畢後,會添加一個新的進程到pool中 print 'Waiting for all subprocesses done...' pool.close() pool.join() #調用join之前,一定要先調用close() 函數,否則會出錯, close()執行後不會有新的進程加入到pool,join函數等待素有子進程結束 print 'All subprocesses done.'
一次執行結果
parent process 7704 Waiting for all subprocesses done... Run task Lee-6948 Run task Marlon-2896 Run task Allen-7304 Run task Frank-3052 Task Lee, runs 1.59 seconds. Task Marlon runs 8.48 seconds. Task Frank runs 15.68 seconds. Task Allen runs 18.08 seconds. All subprocesses done.