Python中的併發程式設計

jobbole發表於2014-04-08

 簡介

  我們將一個正在執行的程式稱為程式。每個程式都有它自己的系統狀態,包含記憶體狀態、開啟檔案列表、追蹤指令執行情況的程式指標以及一個儲存區域性變數的呼叫棧。通常情況下,一個程式依照一個單序列控制流順序執行,這個控制流被稱為該程式的主執行緒。在任何給定的時刻,一個程式只做一件事情。

  一個程式可以通過Python庫函式中的os或subprocess模組建立新程式(例如os.fork()或是subprocess.Popen())。然而,這些被稱為子程式的程式卻是獨立執行的,它們有各自獨立的系統狀態以及主執行緒。因為程式之間是相互獨立的,因此它們同原有的程式併發執行。這是指原程式可以在建立子程式後去執行其它工作。

  雖然程式之間是相互獨立的,但是它們能夠通過名為程式間通訊(IPC)的機制進行相互通訊。一個典型的模式是基於訊息傳遞,可以將其簡單地理解為一個純位元組的緩衝區,而send()或recv()操作原語可以通過諸如管道(pipe)或是網路套接字(network socket)等I/O通道傳輸或接收訊息。還有一些IPC模式可以通過記憶體對映(memory-mapped)機制完成(例如mmap模組),通過記憶體對映,程式可以在記憶體中建立共享區域,而對這些區域的修改對所有的程式可見。

  多程式能夠被用於需要同時執行多個任務的場景,由不同的程式負責任務的不同部分。然而,另一種將工作細分到任務的方法是使用執行緒。同程式類似,執行緒也有其自己的控制流以及執行棧,但執行緒在建立它的程式之內執行,分享其父程式的所有資料和系統資源。當應用需要完成併發任務的時候執行緒是很有用的,但是潛在的問題是任務間必須分享大量的系統狀態。

  當使用多程式或多執行緒時,作業系統負責排程。這是通過給每個程式(或執行緒)一個很小的時間片並且在所有活動任務之間快速迴圈切換來實現的,這個過程將CPU時間分割為小片段分給各個任務。例如,如果你的系統中有10個活躍的程式正在執行,作業系統將會適當的將十分之一的CPU時間分配給每個程式並且迴圈地在十個程式之間切換。當系統不止有一個CPU核時,作業系統能夠將程式排程到不同的CPU核上,保持系統負載平均以實現並行執行。

  利用併發執行機制寫的程式需要考慮一些複雜的問題。複雜性的主要來源是關於同步和共享資料的問題。通常情況下,多個任務同時試圖更新同一個資料結構會造成髒資料和程式狀態不一致的問題(正式的說法是資源競爭的問題)。為了解決這個問題,需要使用互斥鎖或是其他相似的同步原語來標識並保護程式中的關鍵部分。舉個例子,如果多個不同的執行緒正在試圖同時向同一個檔案寫入資料,那麼你需要一個互斥鎖使這些寫操作依次執行,當一個執行緒在寫入時,其他執行緒必須等待直到當前執行緒釋放這個資源。

 Python中的併發程式設計

  Python長久以來一直支援不同方式的併發程式設計,包括執行緒、子程式以及其他利用生成器(generator function)的併發實現。

  Python在大部分系統上同時支援訊息傳遞和基於執行緒的併發程式設計機制。雖然大部分程式設計師對執行緒介面更為熟悉,但是Python的執行緒機制卻有著諸多的限制。Python使用了內部全域性直譯器鎖(GIL)來保證執行緒安全,GIL同時只允許一個執行緒執行。這使得Python程式就算在多核系統上也只能在單個處理器上執行。Python界關於GIL的爭論儘管很多,但在可預見的未來卻沒有將其移除的可能。

  Python提供了一些很精巧的工具用於管理基於執行緒和程式的併發操作。即使是簡單地程式也能夠使用這些工具使得任務併發進行從而加快執行速度。subprocess模組為子程式的建立和通訊提供了API。這特別適合執行與文字相關的程式,因為這些API支援通過新程式的標準輸入輸出通道傳送資料。signal模組將UNIX系統的訊號量機制暴露給使用者,用以在程式之間傳遞事件資訊。訊號是非同步處理的,通常有訊號到來時會中斷程式當前的工作。訊號機制能夠實現粗粒度的訊息傳遞系統,但是有其他更可靠的程式內通訊技術能夠傳遞更復雜的訊息。threading模組為併發操作提供了一系列高階的,物件導向的API。Thread物件們在一個程式內併發地執行,分享記憶體資源。使用執行緒能夠更好地擴充套件I/O密集型的任務。multiprocessing模組同threading模組類似,不過它提供了對於程式的操作。每個程式類是真實的作業系統程式,並且沒有共享記憶體資源,但multiprocessing模組提供了程式間共享資料以及傳遞訊息的機制。通常情況下,將基於執行緒的程式改為基於程式的很簡單,只需要修改一些import宣告即可。

 Threading模組示例

  以threading模組為例,思考這樣一個簡單的問題:如何使用分段並行的方式完成一個大數的累加。

import threading

class SummingThread(threading.Thread):
    def __init__(self, low, high):
        super(SummingThread, self).__init__()
        self.low = low
        self.high = high
        self.total = 0

    def run(self):
        for i in range(self.low, self.high):
            self.total += i

thread1 = SummingThread(0, 500000)
thread2 = SummingThread(500000, 1000000)
thread1.start() # This actually causes the thread to run
thread2.start()
thread1.join()  # This waits until the thread has completed
thread2.join()
# At this point, both threads have completed
result = thread1.total + thread2.total
print(result)

 自定義Threading類庫

  我寫了一個易於使用threads的小型Python類庫,包含了一些有用的類和函式。

  關鍵引數:

  * do_threaded_work – 該函式將一系列給定的任務分配給對應的處理函式(分配順序不確定)

  * ThreadedWorker – 該類建立一個執行緒,它將從一個同步的工作佇列中拉取工作任務並將處理結果寫入同步結果佇列

  * start_logging_with_thread_info – 將執行緒id寫入所有日誌訊息。(依賴日誌環境)

  * stop_logging_with_thread_info – 用於將執行緒id從所有的日誌訊息中移除。(依賴日誌環境)

import threading
import logging

def do_threaded_work(work_items, work_func, num_threads=None, per_sync_timeout=1, preserve_result_ordering=True):
    """ Executes work_func on each work_item. Note: Execution order is not preserved, but output ordering is (optionally).

        Parameters:
        - num_threads               Default: len(work_items)  --- Number of threads to use process items in work_items.
        - per_sync_timeout          Default: 1                --- Each synchronized operation can optionally timeout.
        - preserve_result_ordering  Default: True             --- Reorders result_item to match original work_items ordering.

        Return: 
        --- list of results from applying work_func to each work_item. Order is optionally preserved.

        Example:

        def process_url(url):
            # TODO: Do some work with the url
            return url

        urls_to_process = ["http://url1.com", "http://url2.com", "http://site1.com", "http://site2.com"]

        # process urls in parallel
        result_items = do_threaded_work(urls_to_process, process_url)

        # print(results)
        print(repr(result_items))
    """
    global wrapped_work_func
    if not num_threads:
        num_threads = len(work_items)

    work_queue = Queue.Queue()
    result_queue = Queue.Queue()

    index = 0
    for work_item in work_items:
        if preserve_result_ordering:
            work_queue.put((index, work_item))
        else:
            work_queue.put(work_item)
        index += 1

    if preserve_result_ordering:
        wrapped_work_func = lambda work_item: (work_item[0], work_func(work_item[1]))

    start_logging_with_thread_info()

    #spawn a pool of threads, and pass them queue instance 
    for _ in range(num_threads):
        if preserve_result_ordering:
            t = ThreadedWorker(work_queue, result_queue, work_func=wrapped_work_func, queue_timeout=per_sync_timeout)
        else:
            t = ThreadedWorker(work_queue, result_queue, work_func=work_func, queue_timeout=per_sync_timeout)
        t.setDaemon(True)
        t.start()

    work_queue.join()
    stop_logging_with_thread_info()

    logging.info('work_queue joined')

    result_items = []
    while not result_queue.empty():
        result = result_queue.get(timeout=per_sync_timeout)
        logging.info('found result[:500]: ' + repr(result)[:500])
        if result:
            result_items.append(result)

    if preserve_result_ordering:
        result_items = [work_item for index, work_item in result_items]

    return result_items

class ThreadedWorker(threading.Thread):
    """ Generic Threaded Worker
        Input to work_func: item from work_queue

    Example usage:

    import Queue

    urls_to_process = ["http://url1.com", "http://url2.com", "http://site1.com", "http://site2.com"]

    work_queue = Queue.Queue()
    result_queue = Queue.Queue()

    def process_url(url):
        # TODO: Do some work with the url
        return url

    def main():
        # spawn a pool of threads, and pass them queue instance 
        for i in range(3):
            t = ThreadedWorker(work_queue, result_queue, work_func=process_url)
            t.setDaemon(True)
            t.start()

        # populate queue with data   
        for url in urls_to_process:
            work_queue.put(url)

        # wait on the queue until everything has been processed     
        work_queue.join()

        # print results
        print repr(result_queue)

    main()
    """

    def __init__(self, work_queue, result_queue, work_func, stop_when_work_queue_empty=True, queue_timeout=1):
        threading.Thread.__init__(self)
        self.work_queue = work_queue
        self.result_queue = result_queue
        self.work_func = work_func
        self.stop_when_work_queue_empty = stop_when_work_queue_empty
        self.queue_timeout = queue_timeout

    def should_continue_running(self):
        if self.stop_when_work_queue_empty:
            return not self.work_queue.empty()
        else:
            return True

    def run(self):
        while self.should_continue_running():
            try:
                # grabs item from work_queue
                work_item = self.work_queue.get(timeout=self.queue_timeout)

                # works on item
                work_result = self.work_func(work_item)

                #place work_result into result_queue
                self.result_queue.put(work_result, timeout=self.queue_timeout)

            except Queue.Empty:
                logging.warning('ThreadedWorker Queue was empty or Queue.get() timed out')

            except Queue.Full:
                logging.warning('ThreadedWorker Queue was full or Queue.put() timed out')

            except:
                logging.exception('Error in ThreadedWorker')

            finally:
                #signals to work_queue that item is done
                self.work_queue.task_done()

def start_logging_with_thread_info():
    try:
        formatter = logging.Formatter('[thread %(thread)-3s] %(message)s')
        logging.getLogger().handlers[0].setFormatter(formatter)
    except:
        logging.exception('Failed to start logging with thread info')

def stop_logging_with_thread_info():
    try:
        formatter = logging.Formatter('%(message)s')
        logging.getLogger().handlers[0].setFormatter(formatter)
    except:
        logging.exception('Failed to stop logging with thread info')

 使用示例

from test import ThreadedWorker
from queue import Queue

urls_to_process = ["http://facebook.com", "http://pypix.com"]

work_queue = Queue()
result_queue = Queue()

def process_url(url):
    # TODO: Do some work with the url
    return url

def main():
    # spawn a pool of threads, and pass them queue instance 
    for i in range(5):
        t = ThreadedWorker(work_queue, result_queue, work_func=process_url)
        t.setDaemon(True)
        t.start()

    # populate queue with data   
    for url in urls_to_process:
        work_queue.put(url)

    # wait on the queue until everything has been processed     
    work_queue.join()

    # print results
    print(repr(result_queue))

main()

  原文連結: pypix.com   翻譯: 伯樂線上 - 熊崽Kevin

相關文章