深入學習Python中的併發(二)------高級篇

考慮下面的需求:

我們有一個日誌目錄,裏面全是gzip壓縮的日誌文件。

每個日誌文件的格式是固定的,我們要從中提取出所有訪問過robots.txt文件的主機

1.1.1.1 ------------ [10/june/2012:00:18:50 - 0500] "GET /robots.txt ..." 200 71

2.1.1.3 ------------ [12/june/2013:00:18:50 - 0500] "GET /a.txt ..." 202 73

122.1.1.3 ------------ [12/june/2013:00:18:50 - 0500] "GET /robots.txt ..." 202 73

 

不使用併發,我們會寫出如下的程序代碼:

import gzip
import glob
import io

def find_robots(filename):
    robots = set()
    with gzip.open(filename) as f:
        for line in io.TextIOWrapper(f, encoding='ascii'):
            fields = line.split()
            if fields[6] == '/robots.txt':
                robots.add(fields[0])
    return robots

def find_all_robots(logdir):
    files = glob.glob(logdir+'/*.log.gz')
    all_robots = set()
    for robots in map(find_robots, files):
        all_robots.update(robots)
    return all_robots

上面的程序以map-reduce的風格來編寫。

如果想改寫上面的程序以利用多個CPU核心。只需要把map替換成一個類似的操作,並讓它在concurrent.futures庫中的進程池中執行即可。

下面是稍加修改的代碼:

def find_all_robots(logdir):
    files = glob.glob(logdir+'/*.log.gz')
    all_robots = set()
    with ProcessPoolExecutor as pool:
        for robots in pool.map(find_robots, files):
            all_robots.update(robots)
    return all_robots

ProcessPoolExecutor的典型用法如下:

from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as pool:
    do work in parallel using pool

在底層ProcessPoolExcutor使用N個獨立的進程啓動python解釋器,N爲CPU個數,也可以通過參數N傳遞ProcessPoolExecutor(N).直到with塊中的最後一條語句執行完,ProcessPoolExecutor會退出,退出前會等待所有的任務執行完成。

提交到進程池中的任務必須是函數的形式,有2種方法可以提交任務。如果想並行處理一個列表推導式或者map操作,可以使用pool.map,也可以用submit手動提交一個任務。

def work(x):
    result = '''
    '''
    return result

from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as pool:
    future_result = pool.submit(work)

    r = future_result.result()

手動提交任務會返回一個future對象,可以通過result方法獲取結果,但是result方法會阻塞到結果返回。

爲了不讓其阻塞,還可以安裝一個處理完成函數。

def when_done(r):
    print('Got:', r.result())

with ProcessPoolExecutor() as pool:
    future_result = pool.submit(work)
    future_result.add_done_callback(when_done)

儘管進程池使用起來非常簡單,但是還是要注意下面幾點:

1.這種並行化處理技術只適合於可以將問題分解成各個獨立部分的情況

2.任務只能定義成普通函數來提交,實例方法,閉包或者其他類型的可調用對象都是不支持並行處理的

3.函數的參數和返回值必須可兼容於pickle編碼。任務的執行是在單獨的解釋器進程中執行的,這中間需要用到進程間通信。因此,不同的解釋器間交換數據必須要進行序列化處理

4.提交的工作函數不應該維護持久的狀態或者帶有副作用。

5.在UNIX環境中,進程池是通過fork系統調用實現的

6.當進程池和線程池結合在一起時要格外小心,通常應該在創建線程池前啓動進程池

 

如何規避GIL的限制。

在Python解釋器的C語言實現中,有一部分代碼並不是線程安全的,因此並不能完全地併發執行。事實上,解釋器被一個稱之爲全局解釋器鎖(GIL)的東西保護着,任意時刻只允許有一個python線程執行。GIL帶來的最明顯影響就是多線程的Python程序無法充分利用多核心CPU的優勢(即,一個採用多線程技術的計算密集型應用只能在一個CPU上運行)

要理解GIL,需要知道Python何時釋放GIL。

每當阻塞等待I/O操作時解釋器都會釋放GIL。對於從不執行任何阻塞操作的的CPU密集型線程,Python解釋器會在執行一定數量的字節碼後釋放GIL,以便其他線程得到執行機會。但是C語言擴展模塊則不同,調用C函數時GIL會被鎖定,直到其返回爲止。

由於C代碼是不受解釋器控制的,這一期間不會執行任何Python字節碼,因此解釋器就沒法釋放GIL了。

TALK SO MUCH.要規避GIL限制,通常有2種策略:

1.如果完全使用Python來編程,使用multiprocessing模塊來創建進程池,把它當作協處理器來使用。

2.把重點放在C語言擴展上,主要思想就是在計算密集型任務轉移到C代碼中,使其獨立於Python,並在C代碼中釋放GIL。通過在C代碼中插入特殊的宏來實現:

#include "Python.h"

PyObject *pyfunc(PyObject *self, PyObject *args)
{
    ...
    Py_BEGIN_ALLOW_THREADS
    // Threaded C code
    ...
    Py_END_ALLOW_THREADS
    ...
}

如果使用cyptes庫或者Cython來訪問C代碼,那麼ctypes會自動釋放GIL,不需要我們來干預。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章