Python多進程Pool與Process全局變量區別,以及用Process實現Pool--part2

(1)
1.Pool使用全局變量的問題
問題簡單描述就是無法使用可變的全局變量(比如for循環),可見如下代碼示例:

from multiprocessing import Pool

# def multi_task_1():
#     print(i, '|', global_var)
def multi_task_2():
    print(global_var)

if __name__ == '__main__':
    print('start')
    global_var = "I'm a global variable"
    p_pool = Pool(5)
    for i in range(5):
        p_pool.apply_async(func=multi_task)
    p_pool.close()
    p_pool.join()
    print('end')
##############
multi_task_1結果:
start
end
###############
multi_task_2結果:
start
I'm a global variable
I'm a global variable
I'm a global variable
I'm a global variable
I'm a global variable
end

執行後會發現輸出內容只有start和end,從0-4的數字並沒有打印出來,並且連不變的全局變量global_var都沒有打印,看起來是沒有執行multi_task(實際上是執行了multi_task。如果multi_task有返回值,並且在main中用Pool的get方法獲取返回值時會報錯NameError: global name ‘i’ is not defined)。但是如果在multi_task裏面刪掉可變全局變量i,那麼全局變量global_var還是能打印出來的
以上代碼換用Process實現卻不存在問題,代碼如下:

from multiprocessing import Process

def multi_task():
    print(i, '|', global_var)

if __name__ == '__main__':
    print('start')
    global_var = "I'm a global variable"
    p_list = []
    for i in range(5):
        p = Process(target=multi_task)
        p.start()
        p_list.append(p)
    for p in p_list:
        p.join()
    print('end')
 #####################
 結果:
start
0 | I'm a global variable
1 | I'm a global variable
2 | I'm a global variable
3 | I'm a global variable
4 | I'm a global variable
end

不變的全局變量global_var和變化的全局變量i都能正確的打印出來;
解決辦法有兩種:
其一就是如上換用Process,缺點是失去了進程池的功能(不過放心,後文會有Process實現進程池功能);
其二是在使用Pool的apply_async方法時將i作爲參數傳遞進去;

其二的代碼如下:

from multiprocessing import Pool

def multi_task(var):
    print(var, '|', global_var)

if __name__ == '__main__':
    print('start')
    global_var = "I'm a global variable"
    p_pool = Pool(5)
    for i in range(5):
        p_pool.apply_async(func=multi_task, args=(i,))
    p_pool.close()
    p_pool.join()
    print('end')
####
結果能打印出i;

可見結果與使用Process一致,兩種變量都能正確輸出

注:因是多進程,所以輸出順序並不一定是01234;以及apply_async方法的args參數形式是tuple,所以如果只有一個入參的話要加個逗號,例如:
參數a要寫成:(a,);

(2)用Process實現Pool
使用Process實現進程池功能
用Queue和while True來模擬進程池,具體可見代碼:

import os
import time
import random
from multiprocessing import Process, Queue, Manager

def multi_task(param, r):
    """多進程需要執行的任務"""
    t = random.random() * 5
    pid = os.getpid()
    print('Task %s(pid is %s) will run %s seconds' % (param, pid, t))
    time.sleep(t)
    res = param * param
    r.append(res)
    print('Task %s\' result is %s' % (param, res))

def process_pool(q, r):
    """模擬進程池"""
    while True:
        try:
            param = q.get(False)  #q的數據結構是Queue,相當於有7個任務3個進程完成,一個進程池裏面管理有3個進程;;;
            multi_task(param, r)  #管理3個進程
        except Exception:
            if q.empty():
                break

def your_code():
    p_list = []
    # 先將多進程所要執行的任務的所有參數放入隊列中
    all_task = Queue()
    for task_param in range(7):
        all_task.put(task_param)
    # 結果存儲
    result = Manager().list()
    # 啓動多進程
    for i in range(3):  ###同時開啓三個3個進程分別完成7個任務
        p = Process(target=process_pool, args=(all_task, result))
        p.start()
        p_list.append(p)
    for p in p_list:
        p.join()
    print(result)

if __name__ == '__main__':
    print('start')
    your_code()
    print('end')

'''
start
Task 0(pid is 7863) will run 2.9767118036253604 seconds
Task 1(pid is 7864) will run 1.9310195443844242 seconds
Task 2(pid is 7866) will run 2.1591957030008375 seconds
Task 1' result is 1
Task 3(pid is 7864) will run 0.07803347934412064 seconds
Task 3' result is 9
Task 4(pid is 7864) will run 2.2395319191851164 seconds
Task 2' result is 4
Task 5(pid is 7866) will run 1.7932482530881426 seconds
Task 0' result is 0
Task 6(pid is 7863) will run 3.0386332549169044 seconds
Task 5' result is 25
Task 4' result is 16
Task 6' result is 36
[1, 9, 4, 0, 25, 16, 36]
end

結果分析:
pid只有3個;
任務有7個;
由於Queue()數據結構,
前面3個進程同時開啓,
模擬進程池部分相當於一個進程池管理3個進程,3個進程分別完成7個任務;
'''

(3)類內使用Pool問題
如果在類內直接使用Pool來使用多進程的話,一般會出現問題。示例代碼如下:

from multiprocessing import Pool

global_var = "I'm a global variable"

class SomeClass(object):

    def some_method(self):
        print 'start'
        p_pool = Pool(5)
        for i in range(5):
            p_pool.apply_async(func=self.multi_task, args=(i,))
        p_pool.close()
        p_pool.join()
        print 'end'

    def multi_task(self, var):
        print var, '|', global_var

cls = SomeClass()
cls.some_method()

# output
"""
start
end
"""

雖然參數傳入了,但從結果上來看很像第一個問題(不傳入也是這個結果),看起來是沒有執行multi_task(實際上是執行了multi_task。如果multi_task有返回值,並且在main中用Pool的get方法獲取返回值時會報錯cPickle.PicklingError: Can’t pickle <type ‘instancemethod’>: attribute lookup builtin.instancemethod failed,這也是解釋該問題的原因,實例無法序列化)

通過在網上搜索答案,發現這是因爲Pool中使用Queue來進行通信,所有進入隊列的數據必須可序列化,包括實例方法。而在python2.7中,實例方法並不能被序列化,從而出現該問題(python3中實例方法可以被序列化了,所以這麼使用就沒問題了)

解決方法有多種,最簡單的就是把multi_task放到類外,問題迎刃而解;

from multiprocessing import Pool

global_var = "I'm a global variable"

class SomeClass(object):

    def some_method(self):
        print 'start'
        p_pool = Pool(5)
        for i in range(5):
            p_pool.apply_async(func=multi_task, args=(i,))
        p_pool.close()
        p_pool.join()
        print 'end'

def multi_task(var):
    print var, '|', global_var

cls = SomeClass()
cls.some_method()

# output
"""
start
0 | I'm a global variable
1 | I'm a global variable
3 | I'm a global variable
2 | I'm a global variable
4 | I'm a global variable
end
"""

其他的解決辦法以及更爲細緻解釋和實驗,可以參見以下兩篇文章:
http://xiaorui.cc/2016/01/18/python-multiprocessing%E9%81%87%E5%88%B0cant-pickle-instancemethod%E9%97%AE%E9%A2%98/
https://strcpy.me/index.php/archives/318/

其他事項
1.多進程之間的數據共享
多進程之間不能使用普通的Python數據類型,比如平常使用的list或者dict由父進程傳遞給子進程後,子進程只可讀,寫無效。但是Python在multiprocessing模塊中給我們提供了一些用於多進程的數據類型:

multiprocessing.Queue,是多進程安全的隊列(FIFO),其主要有put和get兩個方法,分別是存儲數據和取出數據。使用方法很簡單。更多信息可自行搜索
multiprocessing.Manager,封裝了可用於多進程的常用數據類型,例如list和dict。使用Manager創建一個list以後,該list的使用和普通list一致。更多信息可自行搜索
還有一些其他的,例如multiprocessing.Array,multiprocessing.Pipe等,因爲沒有使用過所以在此不再贅述,感興趣可自行搜索
在實踐中發現,使用Manager的list時,雖然使用方法上和普通list一樣,但可能因爲多進程之間通信的緣故,list中每個元素大小存在限制。因爲將之前單進程的代碼修改爲多進程後,出現報錯OverflowError: cannot serialize a string larger than 2 GiB multiprocessing,經查看發現這是在某處給Manager的list添加元素時發生的,並且該元素的確很大,而原來單進程的時候卻沒有出現過該問題,所以遂產生剛纔的猜想(因爲沒有找到相關解釋)。我的解決辦法就是把該超大元素切分後再分別添加到Manager的list中

2.Pool子進程不顯示報錯信息
在最一開始使用Pool的時候,發現子進程中如果有錯誤的話並不會拋出異常,而是該子進程直接死掉然後繼續去取任務執行。諸如本文上述的NameError、cPickle.PicklingError、OverflowError都沒有拋出,這在查找問題的時候頗費了一番功夫,而使用Process的時候這些異常均能正常拋出。經實踐發現,只有多進程執行的任務有返回值,並且在主進程用get方法來獲取返回值時,子進程中的異常纔會正常拋出,原因是:apply_async返回的是AsyncResult,其中出現的異常只有在調用AsyncResult.get()的時候纔會被重新引發(來源於今天遇到的Python多線程、多進程中的幾個坑)

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