Python進(jìn)程池log死鎖問(wèn)題分析及解決
背景
最近線上運(yùn)行的一個(gè)python任務(wù)負(fù)責(zé)處理一批數(shù)據(jù),為提高處理效率,使用了python進(jìn)程池,并會(huì)打印log。最近發(fā)現(xiàn),任務(wù)時(shí)常會(huì)出現(xiàn)夯住的情況,當(dāng)查看現(xiàn)場(chǎng)時(shí)發(fā)現(xiàn),夯住時(shí)通常會(huì)有幾個(gè)子進(jìn)程打印了相關(guān)錯(cuò)誤日志,然后整個(gè)任務(wù)就停滯在那里了。
原因
夯住的原因正是由于一行不起眼的log導(dǎo)致,簡(jiǎn)而言之,Python的logging模塊在寫文件模式下,是不支持多進(jìn)程的,強(qiáng)行使用可能會(huì)導(dǎo)致死鎖。
問(wèn)題復(fù)現(xiàn)
可以用下面的代碼來(lái)描述我們遇到的問(wèn)題
import logging from threading import Thread from queue import Queue from logging.handlers import QueueListener, QueueHandler from multiprocessing import Pool ? def setup_logging(): # log的時(shí)候會(huì)寫到一個(gè)隊(duì)列里,然后有一個(gè)單獨(dú)的線程從這個(gè)隊(duì)列里去獲取日志信息并寫到文件里 _log_queue = Queue() QueueListener( _log_queue, logging.FileHandler("out.log")).start() logging.getLogger().addHandler(QueueHandler(_log_queue)) ? # 父進(jìn)程里起一個(gè)單獨(dú)的線程來(lái)寫日志 def write_logs(): while True: logging.info("hello, I just did something") Thread(target=write_logs).start() ? def runs_in_subprocess(): print("About to log...") logging.info("hello, I did something") print("...logged") ? if __name__ == '__main__': setup_logging() ? # 讓一個(gè)進(jìn)程池在死循環(huán)里執(zhí)行,增加觸發(fā)死鎖的幾率 while True: with Pool() as pool: pool.apply(runs_in_subprocess)
我們?cè)趌inux上執(zhí)行該代碼:
About to log... ...logged About to log... ...logged About to log...
發(fā)現(xiàn)程序輸出幾行之后就卡住了。
問(wèn)題出在了哪里
python的進(jìn)程池是基于fork
實(shí)現(xiàn)的,當(dāng)我們只使用fork()
創(chuàng)建子進(jìn)程而不是用execve()
來(lái)替換進(jìn)程上下時(shí),需要注意一個(gè)問(wèn)題:fork()
出來(lái)的子進(jìn)程會(huì)和父進(jìn)程共享內(nèi)存空間,除了父進(jìn)程所擁有的線程。
對(duì)于代碼
from threading import Thread, enumerate from os import fork from time import sleep ? # Start a thread: Thread(target=lambda: sleep(60)).start() ? if fork(): print("The parent process has {} threads".format( len(enumerate()))) else: print("The child process has {} threads".format( len(enumerate())))
輸出:
The parent process has 2 threads
The child process has 1 threads
可以發(fā)現(xiàn),父進(jìn)程中的子線程并沒(méi)有被fork到子進(jìn)程中,而這正是導(dǎo)致死鎖的原因:
- 當(dāng)父進(jìn)程中的線程要向隊(duì)列中寫log時(shí),它需要獲取鎖
- 如果恰好在獲取鎖后進(jìn)行了
fork
操作,那這個(gè)鎖也會(huì)被帶到子進(jìn)程中,同時(shí)這個(gè)鎖的狀態(tài)是占用中 - 這時(shí)候子進(jìn)程要寫日志的話,也需要獲取鎖,但是由于鎖是占用狀態(tài),導(dǎo)致永遠(yuǎn)也無(wú)法獲取,至此,死鎖產(chǎn)生。
如何解決
使用多進(jìn)程共享隊(duì)列
出現(xiàn)上述死鎖的原因之一在于在fork子進(jìn)程的時(shí)候,把隊(duì)列和鎖的狀態(tài)都給fork
過(guò)來(lái)了,那要避免死鎖,一種方案就是使用進(jìn)程共享的隊(duì)列。
import logging import multiprocessing from logging.handlers import QueueListener from time import sleep ? ? def listener_configurer(): root = logging.getLogger() h = logging.handlers.RotatingFileHandler('out.log', 'a', 300, 10) f = logging.Formatter('%(asctime)s %(processName)-10s %(name)s %(levelname)-8s %(message)s') h.setFormatter(f) root.addHandler(h) ? # 從隊(duì)列獲取元素,并寫日志 def listener_process(queue, configurer): configurer() while False: try: record = queue.get() if record is None: break logger = logging.getLogger(record.name) logger.handle(record) except Exception: import sys, traceback print('Whoops! Problem:', file=sys.stderr) traceback.print_exc(file=sys.stderr) ? # 業(yè)務(wù)進(jìn)程的日志配置,使用queueHandler, 將要寫的日志塞入隊(duì)列 def worker_configurer(queue): h = logging.handlers.QueueHandler(queue) root = logging.getLogger() root.addHandler(h) root.setLevel(logging.DEBUG) ? ? def runs_in_subprocess(queue, configurer): configurer(queue) print("About to log...") logging.debug("hello, I did something: %s", multiprocessing.current_process().name) print("...logged, %s",queue.qsize()) ? ? if __name__ == '__main__': queue = multiprocessing.Queue(-1) listener = multiprocessing.Process(target=listener_process, args=(queue, listener_configurer)) listener.start() #父進(jìn)程也持續(xù)寫日志 worker_configurer(queue) def write_logs(): while True: logging.debug("in main process, I just did something") Thread(target=write_logs).start() ? while True: multiprocessing.Process(target=runs_in_subprocess, args=(queue, worker_configurer)).start() sleep(2) ?
在上面代碼中,我們?cè)O(shè)置了一個(gè)進(jìn)程間共享的隊(duì)列,將每個(gè)子進(jìn)程的寫日志操作轉(zhuǎn)換為向隊(duì)列添加元素,然后由單獨(dú)的另一個(gè)進(jìn)程將日志寫入文件。和文章開(kāi)始處的問(wèn)題代碼相比,雖然都使用了隊(duì)列,但此處用的是進(jìn)程共享隊(duì)列,不會(huì)隨著fork
子進(jìn)程而出現(xiàn)多個(gè)拷貝,更不會(huì)出現(xiàn)給子進(jìn)程拷貝了一個(gè)已經(jīng)占用了的鎖的情況。
spawn
出現(xiàn)死鎖的另外一層原因是我們只進(jìn)行了fork
, 但是沒(méi)有進(jìn)行execve
, 即子進(jìn)程仍然和父進(jìn)程享有同樣的內(nèi)存空間導(dǎo)致,因此另一種解決方法是在fork后緊跟著執(zhí)行execve
調(diào)用,對(duì)應(yīng)于python中的spawn
操作,修改后的代碼如下:
if __name__ == '__main__': setup_logging() ? while True: # 使用spawn類型的啟動(dòng) with get_context("spawn").Pool() as pool: pool.apply(runs_in_subprocess)
使用spawn
方法時(shí),父進(jìn)程會(huì)啟動(dòng)一個(gè)新的 Python 解釋器進(jìn)程。 子進(jìn)程將只繼承那些運(yùn)行進(jìn)程對(duì)象的 run()
方法所必須的資源,來(lái)自父進(jìn)程的非必需文件描述符和句柄將不會(huì)被繼承,因此使用此方法啟動(dòng)進(jìn)程會(huì)比較慢,但是安全。
以上就是Python進(jìn)程池log死鎖問(wèn)題分析及解決的詳細(xì)內(nèi)容,更多關(guān)于Python進(jìn)程池log死鎖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
用Python實(shí)現(xiàn)職工信息管理系統(tǒng)
這篇文章主要介紹了用Python實(shí)現(xiàn)職工信息管理系統(tǒng),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12Flask交互基礎(chǔ)(GET、 POST 、PUT、 DELETE)的使用
這篇文章主要介紹了Flask交互基礎(chǔ)(GET、 POST 、PUT、 DELETE)的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Python+drawpad實(shí)現(xiàn)CPU監(jiān)控小程序
這篇文章主要為大家詳細(xì)介紹了如何利用Python+drawpad實(shí)現(xiàn)一個(gè)簡(jiǎn)單的CPU監(jiān)控小程序,文中示例代碼講解詳細(xì),感興趣的小伙伴可以嘗試一下2022-08-08Python Pyqt5多線程更新UI代碼實(shí)例(防止界面卡死)
這篇文章通過(guò)代碼實(shí)例給大家介紹了Python Pyqt5多線程更新UI防止界面卡死的問(wèn)題,代碼簡(jiǎn)單易懂,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2021-12-12Python中用post、get方式提交數(shù)據(jù)的方法示例
最近在學(xué)習(xí)使用Python,發(fā)現(xiàn)網(wǎng)上很少提到如何使用post,所以下面這篇文章主要給大家介紹了關(guān)于Python中用post、get方式提交數(shù)據(jù)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-09-09python使用redis實(shí)現(xiàn)消息隊(duì)列(異步)的實(shí)現(xiàn)完整例程
本文主要介紹了python使用redis實(shí)現(xiàn)消息隊(duì)列(異步)的實(shí)現(xiàn)完整例程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01Python+OpenCV進(jìn)行人臉面部表情識(shí)別
這篇文章主要介紹了通過(guò)Python OpenCV實(shí)現(xiàn)對(duì)人臉面部表情識(shí)別,判斷人是否為笑臉,文中的示例代碼非常詳細(xì),需要的朋友可以參考一下2021-12-12