為啥進程池封裝在裝飾器中不能生效,而多進程可以?

由於字數限制,詳細提問放在了sf上:並發模型 - python將進程池放在裝飾器里為什麼不生效也沒報錯 - SegmentFault,下面的描述也可以看到具體的問題,但是沒放日誌

我想把進程池封裝在裝飾器里,但是它既沒生效也沒報錯

# coding:utf-8
import multiprocessing
import tornado
from tornado.httpclient import AsyncHTTPClient

process_num = 20 # 進程數
url = "https://www.baidu.com"

def handle_request(response):
print str(response)

def run_in_process(process_num):
def _run_in_process(f):
def __run_in_process(*args, **kwargs):
pool = multiprocessing.Pool(processes=process_num)
for i in range(process_num):
pool.apply_async(f, args=args, kwds=kwargs, callback=kwargs.get("callback"))
pool.close()
pool.join()

return __run_in_process
return _run_in_process

@run_in_process(process_num)
def main():
http_client = AsyncHTTPClient()
http_client.fetch(url, callback=handle_request)
global loop
loop = tornado.ioloop.IOLoop.instance()
if loop._running is False:
loop.start()

if __name__ == "__main__":
main()

但是奇怪的是,我用多進程的方式重寫一次,發現是可以生效的,除了run_in_process方法不同,其他都沒改

def run_in_process(process_num):
def _run_in_process(f):
def __run_in_process(*args, **kwargs):
_processes = []
for i in xrange(process_num):
p = multiprocessing.Process(target=f, args=args, kwargs=kwargs)
p.start()
_processes.append(p)

for p in _processes:
p.join()

return __run_in_process
return _run_in_process

同樣的情況也會出現在線程池跟協程的使用上,有誰知道這是怎麼回事嗎?


這個跟multiprocessing的原理有關,也涉及到Python中pickling的一些實現機制。

首先要說明multiprocessing模塊現在有多種實現原理,類Unix系統默認使用fork,原理在於創建Process並啟動的時候進行一次fork,然後子進程執行Process中指定的函數,它可以繼承創建進程時內存中的對象,因此可以指定各種對象給子進程如函數、閉包、socket等

而Python2中Windows操作系統的實現spawn,每次重新啟動一個全新的Python進程。以及Python3中forkserver的實現(在multiprocessing初始化的時候通過fork創建一個forkserver的進程,forkserver進程不再執行後續的代碼,而是等待請求;所有後續創建multiprocessing進程時,都連接forkserver,由forkserver進行fork,產生一個「乾淨」的進程來作為新進程),這兩種實現並不繼承當前運行環境中的對象,所以需要使用的對象必須通過pickling的方法傳遞給子進程。

另外,multiprocessing中的Pool對象,它的原理在於預先創建好許多進程,然後從主進程中接受任務。這些任務因為不是在創建進程(fork)時創建出的,所以必須通過pickling的機制傳遞給子進程。multiprocessing.Queue和multiprocessing.Pipe也同樣是使用pickling模塊在進程間傳遞Python對象。

接下來就要說一下pickling了,pickling模塊可以將Python對象序列化成位元組流,再反序列化回到Python對象。但是它是有一定限制的,並不是所有的對象都可以進行序列化。函數和類在pickling中是不能「直接」序列化的,它們在pickling中序列化的原理在於將函數和類變為:package.module.func_name這樣的字元串,即模塊路徑 + 函數/類名的形式。這就要求反序列化的時候:

  1. 這個模塊可以被import
  2. import後的這個模塊中有這個全局名稱
  3. 而且全局名稱代表的值就是傳遞的值。

然而使用裝飾器修飾的時候,main這個函數其實被修改了一次,變為了修飾之後的函數,但是未修飾的函數和修飾後的函數都共享了__main__.main這個名稱,這導致傳遞的雖然是修飾前的函數,但實際接收方接受了__main__.main這個名稱,然後重新import得到的是修飾後的函數,所以在子進程中執行的也是修飾後的函數。multiprocessing模塊有個安全機制,禁止在multiprocessing創建的子進程中再創建multiprocessing子進程,因此這個修飾過的函數會在.Pool的地方報錯,這個異常沒有傳遞迴主進程,所以看上去就像什麼都沒有做一樣。


推薦閱讀:

進程間通信和線程間通信的區別?
如何利用Python抓取PDF中的某些內容?
pandas 怎麼根據一列的數據的值的情況判斷來生成另外一列的數值?
如何批量獲取年報中數據?
Python的大數運算到底是根據什麼基礎原理或者演算法實現的?

TAG:Python | 進程管理 |