【Python】淺談 multiprocessing

楊奇龍發表於2017-07-02
一前言 
   使用python進行併發處理多臺機器/多個例項的時候,我們可以使用threading ,但是由於著名的存在,實際上threading 並未提供真正有效的併發處理,要充分利用到多核CPU,我們需要使用多程式。Python提供了非常好用的多程式包--。multiprocessing 可以利用multiprocessing.Process物件來建立一個程式,該Process物件與Threading物件的用法基本相同,具有相同的方法(官方原話:"The multiprocessing package mostly replicates the API of the threading module.") 比如:start(),run(),join()的方法。multiprocessing包中也有Lock/Event/Semaphore/Condition/Pipe/Queue類用於程式之間的通訊。話不多說 show me the code!

二使用
2.1 初識異同
下面的程式顯示threading和multiprocessing的在使用方面的異同,相近的函式join(),start(),append() 等,並做同一件事情列印自己的程式pid

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. import os
  4. import threading
  5. import multiprocessing
  6. def printer(msg):
  7.     print(msg, os.getpid())
  8. print('Main begin:', os.getpid())
  9. # threading
  10. record = []
  11. for i in range(5):
  12.     thread = threading.Thread(target=printer, args=('threading',))
  13.     thread.start()
  14.     record.append(thread)
  15. for thread in record:
  16.     thread.join()
  17. # multi-process
  18. record = []
  19. for i in range(5):
  20.     process = multiprocessing.Process(target=printer, args=('multiprocessing',))
  21.     process.start()
  22.     record.append(process)
  23. for process in record:
  24.     process.join()
  25. print('Main end:', os.getpid())
輸出結果

點選(此處)摺疊或開啟

  1. Main begin: 9524
  2. threading 9524
  3. threading 9524
  4. threading 9524
  5. threading 9524
  6. threading 9524
  7. multiprocessing 9539
  8. multiprocessing 9540
  9. multiprocessing 9541
  10. multiprocessing 9542
  11. multiprocessing 9543
  12. Main end: 9524
從例子的結果可以看出多執行緒threading的程式id和主程式(父程式)pid一樣 ,同為9524; 多程式列印的pid每個都不一樣,for迴圈中每建立一個process物件都年開一個程式。其他相關的方法基本類似。

2.2 用法
建立程式的類:
Process([group [, target [, name [, args [, kwargs]]]]]),
target表示呼叫物件,
args表示呼叫物件的位置引數元組。
kwargs表示呼叫物件的字典。
name為程式的別名。
group實質上不使用,為None。
方法:is_alive()、join([timeout])、run()、start()、terminate()。其中,Process以start()啟動某個程式,並自動呼叫run方法.
屬性:authkey、daemon(要透過start()設定,必須設定在方法start之前)、exitcode(程式在執行時為None、如果為–N,表示被訊號N結束)、name、pid。其中daemon是父程式終止後自動終止,且自己不能產生新程式,必須在start()之前設定。

2.3 建立單程式
單執行緒比較簡單,建立一個 Process的例項物件就好,傳入引數 target 為已經定義好的方法worker以及worker需要的引數

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. """
  4. author: yangyi@youzan.com
  5. time: 2017/7/2 下午6:45
  6. func:
  7. """
  8. import multiprocessing
  9. import datetime, time
  10. def worker(interval):
  11.     print("process start: {0}".format(datetime.datetime.today()));
  12.     time.sleep(interval)
  13.     print("process end: {0}".format(datetime.datetime.today()));

  14. if __name__ == "__main__":
  15.     p = multiprocessing.Process(target=worker, args=(5,))
  16.     p.start()
  17.     p.join()
  18.     print "end!"
2.4 建立多程式

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. """
  4. author: yangyi@youzan.com
  5. time: 2017/7/2 下午7:50
  6. func:
  7. """
  8. import multiprocessing
  9. def worker(num):
  10.     print "worker %d" %num


  11. if __name__ == "__main__":
  12.     print("The number of CPU is:" + str(multiprocessing.cpu_count()))
  13.     proc = []
  14.     for i in xrange(5):
  15.         p = multiprocessing.Process(target=worker, args=(i,))
  16.         proc.append(p)
  17.     for p in proc:
  18.         p.start()
  19.     for p in proc:
  20.         p.join()
  21.     print "end ..."
輸出

點選(此處)摺疊或開啟

  1. The number of CPU is:4
  2. worker 0
  3. worker 1
  4. worker 2
  5. worker 3
  6. worker 4
  7. main process end ...
2.5 執行緒池
multiprocessing提供程式池的類--Pool,它可以指定程式最大可以呼叫的程式數量,當有新的請求提交到pool中時,如果程式池還沒有滿,那麼就會建立一個新的程式用來執行該請求;但如果程式池中的程式數已經達到規定最大值,那麼該請求就會等待,直到池中有程式結束,才會建立新的程式來它。
構造方法:
Pool([processes[, initializer[, initargs[, maxtasksperchild[, context]]]]])
processes  : 使用的工作程式的數量,如果processes是None,預設使用os.cpu_count()返回的數量。
initializer: 如果initializer是None,那麼每一個工作程式在開始的時候會呼叫initializer(*initargs)。
maxtasksperchild:工作程式退出之前可以完成的任務數,完成後用一個新的工作程式來替代原程式,來讓閒置的資源被釋放。maxtasksperchild預設是None,意味著只要Pool存在工作程式就會一直存活。
context: 用在制定工作程式啟動時的上下文,一般使用multiprocessing.Pool()或者一個context物件的Pool()方法來建立一個池,兩種方法都適當的設定了context。

例項方法:
  apply(func[, args[, kwds]]):同步程式池
  apply_async(func[, args[, kwds[, callback[, error_callback]]]]) :非同步程式池
  close() : 關閉程式池,阻止更多的任務提交到pool,待任務完成後,工作程式會退出。
  terminate() : 結束工作程式,不在處理未完成的任務.
  join() : 等待工作執行緒的退出,在呼叫join()前必須呼叫close()或者 terminate(),因為被終止的程式需要被父程式呼叫wait(join等價與wait),否則程式會成為殭屍程式。

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. """
  4. author: yangyi@youzan.com
  5. time: 2017/7/2 下午7:50
  6. func:
  7. """
  8. from multiprocessing import Pool
  9. import time
  10. def worker(num):
  11.     print "worker %d" %num
  12.     time.sleep(2)
  13.     print "end worker %d" %num

  14. if __name__ == "__main__":
  15.     proc_pool = Pool(2)
  16.     for i in xrange(4):
  17.         proc_pool.apply_async(worker, (i,)) #使用了非同步呼叫,從輸出結果可以看出來

  18.     proc_pool.close()
  19.     proc_pool.join()
  20.     print "main process end ..."
輸出結果

點選(此處)摺疊或開啟

  1. worker 0
  2. worker 1
  3. end worker 0
  4. end worker 1
  5. worker 2
  6. worker 3
  7. end worker 2
  8. end worker 3
  9. main process end ..
解釋:建立一個程式池pool 物件proc_pool,並設定程式的數量為2,xrange(4)會相繼產生四個物件[0, 1, 2, 4],四個物件被提交到pool中,因pool指定程式數為2,所以0、1會直接送到程式中執行,當其中的2個任務執行完之後才空出2程式處理物件2和3,所以會出現輸出 worker 2 worker 3 出現在end worker 0 end worker 1之後。思考一下如果呼叫  proc_pool.apply(worker, (i,)) 的輸出結果會是什麼樣的?

2.6 使用queue
multiprocessing提供佇列類,可以透過呼叫multiprocessing.Queue(maxsize) 初始化佇列物件,maxsize表示佇列裡面最多的元素個數。
例子 建立了兩個函式入隊,出隊,出隊處理時使用了lock特性,序列化取資料。
  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. """
  4. author: yangyi@youzan.com
  5. time: 2017/7/2 下午9:03
  6. func:
  7. """
  8. import time
  9. from multiprocessing import Process, current_process,Lock,Queue
  10. import datetime
  11. def inputQ(queue):
  12.     time.sleep(1)
  13.     info = "proc_name: " + current_process().name + ' was putted in queue at: ' + str(datetime.datetime.today())
  14.     queue.put(info)
  15. def outputQ(queue,lock):
  16.     info = queue.get()
  17.     lock.acquire()
  18.     print ("proc_name: " + current_process().name + ' gets info :' + info)
  19.     lock.release()
  20. if __name__ == '__main__':
  21.     record1 = [] # store input processes
  22.     record2 = [] # store output processes
  23.     lock = Lock() # To prevent messy print
  24.     queue = Queue(3)
  25.     for i in range(10):
  26.         process = Process(target=inputQ, args=(queue,))
  27.         process.start()
  28.         record1.append(process)
  29.     for i in range(10):
  30.         process = Process(target=outputQ, args=(queue,lock))
  31.         process.start()
  32.         record2.append(process)
  33.     for p in record1:
  34.         p.join()
  35.     queue.close() # No more object will come, close the queue
  36.     for p in record2:
  37.         p.join()
2.7 使用pipe 
Pipe可以是單向(half-duplex),也可以是雙向(duplex)。我們透過mutiprocessing.Pipe(duplex=False)建立單向管道 (預設為雙向)。一個程式從PIPE一端輸入物件,然後被PIPE另一端的程式接收,單向管道只允許管道一端的程式輸入,而雙向管道則允許從兩端輸入。
用法 multiprocessing.Pipe([duplex])
該類返回一組物件例項(conn1, conn2),分別代表傳送和接受訊息的兩端。

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. """
  4. author: yangyi@youzan.com
  5. time: 2017/7/2 下午8:01
  6. func:
  7. """
  8. from multiprocessing import Process, Pipe
  9. def p1(conn, name):
  10.     conn.send('hello ,{name}'.format(name=name))
  11.     print "p1 receive :", conn.recv()
  12.     conn.close()

  13. def p2(conn, name):
  14.     conn.send('hello ,{name}'.format(name=name))
  15.     print "p2 receive :", conn.recv()
  16.     conn.close()

  17. if __name__ == '__main__':
  18.     parent_conn, child_conn = Pipe()
  19.     proc1 = Process(target=p1, args=(child_conn, "parent_conn"))
  20.     proc2 = Process(target=p2, args=(parent_conn, "child_conn"))
  21.     proc1.start()
  22.     proc2.start()
  23.     proc1.join()
  24.     proc2.join()
輸出:

點選(此處)摺疊或開啟

  1. p1 receive : hello ,child_conn
  2. p2 receive : hello ,parent_conn
該例子中 p1 p2 透過pipe 給彼此相互傳送資訊,p1 傳送"parent_conn" 給 p2 ,p2 傳送"child_conn" 給p1.
2.8 daemon程式對比結果
  1. import multiprocessing
  2. import datetime, time
  3. def worker(interval):
  4.     print("process start: {0}".format(datetime.datetime.today()));
  5.     time.sleep(interval)
  6.     print("process end: {0}".format(datetime.datetime.today()));
  7. if __name__ == "__main__":
  8.     p = multiprocessing.Process(target=worker, args=(5,))
  9.     p.start()
  10.     print "end!"
輸出:

點選(此處)摺疊或開啟

  1. end!
  2. process start: 2017-07-02 18:47:30.656244
  3. process end: 2017-07-02 18:47:35.657464

設定 daemon = True,程式隨著主程式結束而不等待子程式。
  1. import multiprocessing
  2. import datetime, time
  3. def worker(interval):
  4.     print("process start: {0}".format(datetime.datetime.today()));
  5.     time.sleep(interval)
  6.     print("process end: {0}".format(datetime.datetime.today()));
  7. if __name__ == "__main__":
  8.     p = multiprocessing.Process(target=worker, args=(5,))
  9.     p.daemon = True
  10.     p.start()
  11.     print "end!"
輸出:
end!
因為子程式設定了daemon屬性,主程式結束,multiprocessing建立的程式物件就隨著結束了。

  1. import multiprocessing
  2. import datetime, time
  3. def worker(interval):
  4.     print("process start: {0}".format(datetime.datetime.today()));
  5.     time.sleep(interval)
  6.     print("process end: {0}".format(datetime.datetime.today()));
  7. if __name__ == "__main__":
  8.     p = multiprocessing.Process(target=worker, args=(5,))
  9.     p.daemon = True  #
  10.     p.start()
  11.     p.join() #程式執行完畢後再關閉
  12.     print "end!"
輸出:

點選(此處)摺疊或開啟

  1. process start: 2017-07-02 18:48:20.953754
  2. process end: 2017-07-02 18:48:25.954736

2.9 Lock()
當多個程式需要訪問共享資源的時候,Lock可以用來避免訪問的衝突。
例項方法:
acquire([timeout]): 使執行緒進入同步阻塞狀態,嘗試獲得鎖定。
release(): 釋放鎖。使用前執行緒必須已獲得鎖定,否則將丟擲異常。
例子:
多個程式使用同一個std_out ,使用lock機制確保同一個時刻有一個一個程式獲取輸出。

  1. #!/usr/bin/env python
    # encoding: utf-8
    """
    author: yangyi@youzan.com
    time: 2017/7/2 下午9:28
    func: 
    """
    from multiprocessing import Process, Lock
    def func_with_lock(l, i):
        l.acquire()
        print 'hello world', i
        l.release()


    def func_without_lock(i):
        print 'hello world', i


    if __name__ == '__main__':
        lock = Lock()
        print "func_with_lock :"
        for num in range(10):
            Process(target=func_with_lock, args=(lock, num)).start()


輸出:

點選(此處)摺疊或開啟

  1. func_with_lock :
  2. hello world 0
  3. hello world 1
  4. hello world 2
  5. hello world 3
  6. hello world 4
  7. hello world 5
  8. hello world 6
  9. hello world 7
  10. hello world 8
  11. hello world 9

三 小結
 本文參考官方資料以及其他資源,對multiprocesssing 的使用方式做了總結,還有很多知識需要詳細閱讀官方文件。紙上來得終覺淺,絕知此事要躬行。參考資料
[1] 
[2]Python標準庫10 多程式初步 (multiprocessing包)

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/22664653/viewspace-2141502/,如需轉載,請註明出處,否則將追究法律責任。

相關文章