Python2.6版本中新添了multiprocessing模組。它最初由Jesse Noller和Richard Oudkerk定義在PEP 371中。就像你能通過threading模組衍生執行緒一樣,multiprocessing 模組允許你衍生程式。這裡用到的思想:因為你現在能衍生程式,所以你能夠避免使用全域性直譯器鎖(GIL),並且充分利用機器的多個處理器。
多程式包也包含一些根本不在threading 模組中的API。比如:有一個靈活的Pool類能讓你在多個輸入下並行化地執行函式。我們將在後面的小節講解Pool類。我們將以multiprocessing模組的Process類開始講解。
…………………………………………………………………………………………………………………………
開始學習multiprocessing模組
Process這個類和threading模組中的Thread類很像。讓我們建立一系列呼叫相同函式的程式,並且看看它是如何工作的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import os from multiprocessing import Process def doubler(number): """ A doubling function that can be used by a process """ result = number * 2 proc = os.getpid() print('{0} doubled to {1} by process id: {2}'.format( number, result, proc)) if __name__ == '__main__': numbers = [5, 10, 15, 20, 25] procs = [] for index, number in enumerate(numbers): proc = Process(target=doubler, args=(number,)) procs.append(proc) proc.start() for proc in procs: proc.join() |
對於上面的例子,我們匯入Process類、建立一個叫doubler的函式。在函式中,我們將傳入的數字乘上2。我們也用Python的os模組來獲取當前程式的ID(pid)。這個ID將告訴我們哪個程式正在呼叫doubler函式。然後,在下面的程式碼塊中,我們例項化了一系列的Process類並且啟動它們。最後一個迴圈只是呼叫每個程式的join()方法,該方法告訴Python等待程式直到它結束。如果你需要結束一個程式,你可以呼叫它的terminate()方法。
當你執行上面的程式碼,你應該看到和下面類似的輸出結果:
1 2 3 4 5 |
5 doubled to 10 by process id: 10468 10 doubled to 20 by process id: 10469 15 doubled to 30 by process id: 10470 20 doubled to 40 by process id: 10471 25 doubled to 50 by process id: 10472 |
有時候,你最好給你的程式取一個易於理解的名字 。幸運的是,Process類確實允許你訪問同樣的程式。讓我們來看看如下例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import os from multiprocessing import Process, current_process def doubler(number): """ A doubling function that can be used by a process """ result = number * 2 proc_name = current_process().name print('{0} doubled to {1} by: {2}'.format( number, result, proc_name)) if __name__ == '__main__': numbers = [5, 10, 15, 20, 25] procs = [] proc = Process(target=doubler, args=(5,)) for index, number in enumerate(numbers): proc = Process(target=doubler, args=(number,)) procs.append(proc) proc.start() proc = Process(target=doubler, name='Test', args=(2,)) proc.start() procs.append(proc) for proc in procs: proc.join() |
這一次,我們多匯入了current_process。current_process基本上和threading模組的current_thread是類似的東西。我們用它來獲取正在呼叫我們的函式的執行緒的名字。你將注意到我們沒有給前面的5個程式設定名字。然後我們將第6個程式的名字設定為“Test”。
讓我們看看我們將得到什麼樣的輸出結果:
1 2 3 4 5 6 |
5 doubled to 10 by: Process-2 10 doubled to 20 by: Process-3 15 doubled to 30 by: Process-4 20 doubled to 40 by: Process-5 25 doubled to 50 by: Process-6 2 doubled to 4 by: Test |
輸出結果說明:預設情況下,multiprocessing模組給每個程式分配了一個編號,而該編號被用來組成程式的名字的一部分。當然,如果我們給定了名字的話,並不會有編號被新增到名字中。
鎖
multiprocessing模組支援鎖,它和threading模組做的方式一樣。你需要做的只是匯入Lock,獲取它,做一些事,釋放它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from multiprocessing import Process, Lock def printer(item, lock): """ Prints out the item that was passed in """ lock.acquire() try: print(item) finally: lock.release() if __name__ == '__main__': lock = Lock() items = ['tango', 'foxtrot', 10] for item in items: p = Process(target=printer, args=(item, lock)) p.start() |
我們在這裡建立了一個簡單的用於列印函式,你輸入什麼,它就輸出什麼。為了避免執行緒之間互相阻塞,我們使用Lock物件。程式碼迴圈列表中的三個項併為它們各自都建立一個程式。每一個程式都將呼叫我們的函式,並且每次遍歷到的那一項作為引數傳入函式。因為我們現在使用了鎖,所以佇列中下一個程式將一直阻塞,直到之前的程式釋放鎖。
日誌
為程式建立日誌與為執行緒建立日誌有一些不同。它們存在不同是因為Python的logging包不使用共享鎖的程式,因此有可能以來自不同程式的資訊作為結束的標誌。讓我們試著給前面的例子新增基本的日誌。程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import logging import multiprocessing from multiprocessing import Process, Lock def printer(item, lock): """ Prints out the item that was passed in """ lock.acquire() try: print(item) finally: lock.release() if __name__ == '__main__': lock = Lock() items = ['tango', 'foxtrot', 10] multiprocessing.log_to_stderr() logger = multiprocessing.get_logger() logger.setLevel(logging.INFO) for item in items: p = Process(target=printer, args=(item, lock)) p.start() |
最簡單的新增日誌的方法通過推送它到stderr實現。我們能通過呼叫thelog_to_stderr() 函式來實現該方法。然後我們呼叫get_logger 函式獲得一個logger例項,並將它的日誌等級設為INFO。之後的程式碼是相同的。需要提示下這裡我並沒有呼叫join()方法。取而代之的:當它退出,父執行緒將自動呼叫join()方法。
當你這麼做了,你應該得到類似下面的輸出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[INFO/Process-1] child process calling self.run() tango [INFO/Process-1] process shutting down [INFO/Process-1] process exiting with exitcode 0 [INFO/Process-2] child process calling self.run() [INFO/MainProcess] process shutting down foxtrot [INFO/Process-2] process shutting down [INFO/Process-3] child process calling self.run() [INFO/Process-2] process exiting with exitcode 0 10 [INFO/MainProcess] calling join() for process Process-3 [INFO/Process-3] process shutting down [INFO/Process-3] process exiting with exitcode 0 [INFO/MainProcess] calling join() for process Process-2 |
現在如果你想要儲存日誌到硬碟中,那麼這件事就顯得有些棘手。你能在Python的logging Cookbook閱讀一些有關那類話題。
Pool類
Pool類被用來代表一個工作程式池。它有讓你將任務轉移到工作程式的方法。讓我們看下面一個非常簡單的例子。
1 2 3 4 5 6 7 8 9 |
from multiprocessing import Pool def doubler(number): return number * 2 if __name__ == '__main__': numbers = [5, 10, 20] pool = Pool(processes=3) print(pool.map(doubler, numbers)) |
基本上執行上述程式碼之後,一個Pool的例項被建立,並且該例項建立了3個工作程式。然後我們使用map 方法將一個函式和一個可迭代物件對映到每個程式。最後我們列印出這個例子的結果:[10, 20, 40]。
你也能通過apply_async方法獲得池中程式的執行結果:
1 2 3 4 5 6 7 8 9 |
from multiprocessing import Pool def doubler(number): return number * 2 if __name__ == '__main__': pool = Pool(processes=3) result = pool.apply_async(doubler, (25,)) print(result.get(timeout=1)) |
我們上面做的事實際上就是請求程式的執行結果。那就是get函式的用途。它嘗試去獲取我們的結果。你能夠注意到我們設定了timeout,這是為了預防我們呼叫的函式發生異常的情況。畢竟我們不想要它被無限期地阻塞。
程式通訊
當遇到程式間通訊的情況,multiprocessing 模組提供了兩個主要的方法:Queues 和 Pipes。Queue 實現上既是執行緒安全的也是程式安全的。讓我們看一個相當簡單的並且基於 Queue的例子。程式碼來自於我的文章(threading articles)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
from multiprocessing import Process, Queue sentinel = -1 def creator(data, q): """ Creates data to be consumed and waits for the consumer to finish processing """ print('Creating data and putting it on the queue') for item in data: q.put(item) def my_consumer(q): """ Consumes some data and works on it In this case, all it does is double the input """ while True: data = q.get() print('data found to be processed: {}'.format(data)) processed = data * 2 print(processed) if data is sentinel: break if __name__ == '__main__': q = Queue() data = [5, 10, 13, -1] process_one = Process(target=creator, args=(data, q)) process_two = Process(target=my_consumer, args=(q,)) process_one.start() process_two.start() q.close() q.join_thread() process_one.join() process_two.join() |
在這裡我們只需要匯入Queue和Process。Queue用來建立資料和新增資料到佇列中,Process用來消耗資料並執行它。通過使用Queue的put()和get()方法,我們就能新增資料到Queue、從Queue獲取資料。程式碼的最後一塊只是建立了Queue 物件以及兩個Process物件,並且執行它們。你能注意到我們在程式物件上呼叫join()方法,而不是在Queue本身上呼叫。
總結
我們這裡有大量的資料。你已經學習如何使用multiprocessing模組指定不變的函式、使用Queues在程式間通訊、給程式命名等很多事。在Python文件中也有很多本文沒有接觸到的知識點,因此也務必深入瞭解下文件。與此同時,你現在知道如何用Python利用你電腦所有的處理能力了!
相關閱讀
- 有關multiprocessing模組的Python文件(multiprocessing module)
- Python模組週刊:multiprocessing
- Python的併發–Porting a Queue to multiprocessing