簡體   English   中英

Python JoinableQueue 在其他進程中調用 task_done 需要兩次

[英]Python JoinableQueue call task_done in other process need twice

我已經實現了一個基於multiprocessing.ProcessJoinableQueue的 WorkerManager。 雖然我嘗試在 proc.join(timeout) 之后處理超時或取消處理異常等進程異常,並評估 proc.exitcode 以確定如何處理,然后調用 in_queue.task_done() 以通知作業已完成異常處理邏輯。 但是它需要調用兩次。 我不知道為什么它應該被調用兩次。 有沒有人可以在這里找出原因。

整個代碼片段:

# -*- coding=utf-8 -*-

import time
import threading
from queue import Empty
from multiprocessing import Event, Process, JoinableQueue, cpu_count, current_process

TIMEOUT = 3


class WorkersManager(object):

    def __init__(self, jobs, processes_num):
        self._processes_num = processes_num if processes_num else cpu_count()
        self._workers_num = processes_num
        self._in_queue, self._run_queue, self._out_queue = JoinableQueue(), JoinableQueue(), JoinableQueue()
        self._spawned_procs = []
        self._total = 0
        self._stop_event = Event()
        self._jobs_on_procs = {}

        self._wk_kwargs = dict(
            in_queue=self._in_queue, run_queue=self._run_queue, out_queue=self._out_queue,
            stop_event=self._stop_event
        )

        self._in_stream = [j for j in jobs]
        self._out_stream = []
        self._total = len(self._in_stream)

    def run(self):
        # Spawn Worker
        worker_processes = [
            WorkerProcess(i, **self._wk_kwargs) for i in range(self._processes_num)
        ]
        self._spawned_procs = [
            Process(target=process.run, args=tuple())
            for process in worker_processes
        ]

        for p in self._spawned_procs:
            p.start()

        self._serve()

        monitor = threading.Thread(target=self._monitor, args=tuple())
        monitor.start()

        collector = threading.Thread(target=self._collect, args=tuple())
        collector.start()

        self._join_workers()
        # TODO: Terminiate threads
        monitor.join(TIMEOUT)
        collector.join(TIMEOUT)

        self._in_queue.join()
        self._out_queue.join()
        return self._out_stream

    def _join_workers(self):
        for p in self._spawned_procs:
            p.join(TIMEOUT)

            if p.is_alive():
                p.terminate()
                job = self._jobs_on_procs.get(p.name)
                print('Process TIMEOUT: {0} {1}'.format(p.name, job))
                result = {
                    "status": "failed"
                }

                self._out_queue.put(result)
                for _ in range(2):
                    # NOTE: Call task_done twice
                    # Guessing:
                    # 1st time to swtich process?
                    # 2nd time to notify task has done?
                    # TODO: figure it out why?
                    self._in_queue.task_done()
            else:
                if p.exitcode == 0:
                    print("{} exit with code:{}".format(p, p.exitcode))
                else:
                    job = self._jobs_on_procs.get(p.name)
                    if p.exitcode > 0:
                        print("{} with code:{} {}".format(p, p.exitcode, job))
                    else:
                        print("{} been killed with code:{} {}".format(p, p.exitcode, job))

                    result = {
                        "status": "failed"
                    }

                    self._out_queue.put(result)
                    for _ in range(2):
                        # NOTE: Call task_done twice
                        # Guessing:
                        # 1st time to swtich process?
                        # 2nd time to notify task has done?
                        # TODO: figure it out why?
                        self._in_queue.task_done()

    def _collect(self):
        # TODO: Spawn a collector proc
        while True:
            try:
                r = self._out_queue.get()
                self._out_stream.append(r)
                self._out_queue.task_done()

                if len(self._out_stream) >= self._total:
                    print("Total {} jobs done.".format(len(self._out_stream)))
                    self._stop_event.set()
                    break
            except Empty:
                continue

    def _serve(self):
        for job in self._in_stream:
            self._in_queue.put(job)

        for _ in range(self._workers_num):
            self._in_queue.put(None)

    def _monitor(self):
        running = 0
        while True:
            proc_name, job = self._run_queue.get()
            running += 1
            self._jobs_on_procs.update({proc_name: job})
            self._run_queue.task_done()
            if running == self._total:
                break


class WorkerProcess(object):

    def __init__(self, worker_id, in_queue, run_queue, out_queue, stop_event):
        self._worker_id = worker_id
        self._in_queue = in_queue
        self._run_queue = run_queue
        self._out_queue = out_queue
        self._stop_event = stop_event

    def run(self):
        self._work()
        print('worker - {} quit'.format(self._worker_id))

    def _work(self):
        print("worker - {0} start to work".format(self._worker_id))
        job = {}
        while not self._stop_event.is_set():
            try:
                job = self._in_queue.get(timeout=.01)
            except Empty:
                continue

            if not job:
                self._in_queue.task_done()
                break

            try:
                proc = current_process()
                self._run_queue.put((proc.name, job))
                r = self._run_job(job)
                self._out_queue.put(r)
            except Exception as err:
                print('Unhandle exception: {0}'.format(err), exc_info=True)
                result = {"status": 'failed'}
                self._out_queue.put(result)
            finally:
                self._in_queue.task_done()

    def _run_job(self, job):
        time.sleep(job)
        return {
            'status': 'succeed'
        }


def main():

    jobs = [3, 4, 5, 6, 7]
    procs_num = 3
    m = WorkersManager(jobs, procs_num)
    m.run()


if __name__ == "__main__":
    main()

問題代碼如下:

   self._out_queue.put(result)
                    for _ in range(2):
                        # ISSUE HERE !!!
                        # NOTE: Call task_done twice
                        # Guessing:
                        # 1st time to swtich process?
                        # 2nd time to notify task has done?
                        # TODO: figure it out why?
                        self._in_queue.task_done()

我需要調用self._in_queue.task_done()兩次以通知 JoinableQueue 作業已由異常處理邏輯完成。

我猜task_done()第一次調用是否是切換進程上下文? 或其他任何東西。 根據測試。 第二個 task_done() 生效。

worker - 0 start to work
worker - 1 start to work
worker - 2 start to work

Process TIMEOUT: Process-1 5
Process TIMEOUT: Process-2 6
Process TIMEOUT: Process-3 7
Total 5 jobs done.

如果您調用 task_done() 一次,它將永遠阻塞並且不會完成。

問題是您有一個競爭條件,定義為:

當計算機程序要正確運行取決於程序進程或線程的順序或時間時,軟件中就會出現競爭條件。

在方法WorkerProcess._work中,您的主循環開始:

    while not self._stop_event.is_set():
        try:
            job = self._in_queue.get(timeout=.01)
        except Empty:
            continue

        if not job:
            self._in_queue.task_done()
            break

self._stop_event_collect線程設置。 根據發生這種情況時WorkerProcess._work在循環中的位置,它可以退出循環,留下已放置在_in_queue上的None表示不再有作業。 顯然,這對於兩個進程發生了兩次。 它甚至可能發生在 0、1、2 或 3 個進程中。

修復方法是將while not self._stop_event.is_set():替換為while True:並僅依靠在_in_queue上找到None來表示終止。 這使您能夠為那些已正常完成的進程刪除對task_done的額外調用(實際上,每個成功完成的進程只需要一個額外的調用,而不是您擁有的兩個)。

但這只是問題的一半。 另一半是你的代碼:

def _join_workers(self):
    for p in self._spawned_procs:
        p.join(TIMEOUT)
        ...
            p.terminate()

因此,您沒有讓您的工作人員有足夠的時間來耗盡_in_queue ,因此可能會在其上留下任意數量的消息(在您的示例中,當然,只有當前的“工作”是處理和None前哨共2)。

但這是代碼的一般問題:它被過度設計了。 例如,請參考上面的第一個代碼片段。 可以進一步簡化為:

    while True:
        job = self._in_queue.get() # blocking get
        if not job:
            break

此外,甚至沒有理由使用JoinableQueueEvent實例,因為使用放置在_in_queue上的None哨兵足以表示工作進程應該終止,特別是如果您要過早終止工作進程。 簡化的工作代碼是:

import time
import threading
from multiprocessing import Process, Queue, cpu_count, current_process

TIMEOUT = 3


class WorkersManager(object):

    def __init__(self, jobs, processes_num):
        self._processes_num = processes_num if processes_num else cpu_count()
        self._workers_num = processes_num
        self._in_queue, self._run_queue, self._out_queue = Queue(), Queue(), Queue()
        self._spawned_procs = []
        self._total = 0
        self._jobs_on_procs = {}

        self._wk_kwargs = dict(
            in_queue=self._in_queue, run_queue=self._run_queue, out_queue=self._out_queue
        )

        self._in_stream = [j for j in jobs]
        self._out_stream = []
        self._total = len(self._in_stream)

    def run(self):
        # Spawn Worker
        worker_processes = [
            WorkerProcess(i, **self._wk_kwargs) for i in range(self._processes_num)
        ]
        self._spawned_procs = [
            Process(target=process.run, args=tuple())
            for process in worker_processes
        ]

        for p in self._spawned_procs:
            p.start()

        self._serve()

        monitor = threading.Thread(target=self._monitor, args=tuple())
        monitor.start()

        collector = threading.Thread(target=self._collect, args=tuple())
        collector.start()

        self._join_workers()
        # TODO: Terminiate threads
        monitor.join()
        collector.join()

        return self._out_stream

    def _join_workers(self):
        for p in self._spawned_procs:
            p.join(TIMEOUT)

            if p.is_alive():
                p.terminate()
                job = self._jobs_on_procs.get(p.name)
                print('Process TIMEOUT: {0} {1}'.format(p.name, job))
                result = {
                    "status": "failed"
                }

                self._out_queue.put(result)
            else:
                if p.exitcode == 0:
                    print("{} exit with code:{}".format(p, p.exitcode))
                else:
                    job = self._jobs_on_procs.get(p.name)
                    if p.exitcode > 0:
                        print("{} with code:{} {}".format(p, p.exitcode, job))
                    else:
                        print("{} been killed with code:{} {}".format(p, p.exitcode, job))

                    result = {
                        "status": "failed"
                    }

                    self._out_queue.put(result)

    def _collect(self):
        # TODO: Spawn a collector proc
        while True:
            r = self._out_queue.get()
            self._out_stream.append(r)
            if len(self._out_stream) >= self._total:
                print("Total {} jobs done.".format(len(self._out_stream)))
                break

    def _serve(self):
        for job in self._in_stream:
            self._in_queue.put(job)

        for _ in range(self._workers_num):
            self._in_queue.put(None)

    def _monitor(self):
        running = 0
        while True:
            proc_name, job = self._run_queue.get()
            running += 1
            self._jobs_on_procs.update({proc_name: job})
            if running == self._total:
                break


class WorkerProcess(object):

    def __init__(self, worker_id, in_queue, run_queue, out_queue):
        self._worker_id = worker_id
        self._in_queue = in_queue
        self._run_queue = run_queue
        self._out_queue = out_queue

    def run(self):
        self._work()
        print('worker - {} quit'.format(self._worker_id))

    def _work(self):
        print("worker - {0} start to work".format(self._worker_id))
        job = {}
        while True:
            job = self._in_queue.get()
            if not job:
                break

            try:
                proc = current_process()
                self._run_queue.put((proc.name, job))
                r = self._run_job(job)
                self._out_queue.put(r)
            except Exception as err:
                print('Unhandle exception: {0}'.format(err), exc_info=True)
                result = {"status": 'failed'}
                self._out_queue.put(result)

    def _run_job(self, job):
        time.sleep(job)
        return {
            'status': 'succeed'
        }


def main():

    jobs = [3, 4, 5, 6, 7]
    procs_num = 3
    m = WorkersManager(jobs, procs_num)
    m.run()


if __name__ == "__main__":
    main()

印刷:

worker - 0 start to work
worker - 1 start to work
worker - 2 start to work
Process TIMEOUT: Process-1 3
Process TIMEOUT: Process-2 6
Process TIMEOUT: Process-3 7
Total 5 jobs done.

您可能知道這一點,但盡職調查要求我提到有兩個出色的類multiprocessing.Poolconcurrent.futures.ProcessPoolExecutor可以完成您想要完成的工作。 請參閱進行一些比較。

進一步說明

使用支持調用task_doneJoinableQueue有什么意義? 通常,這是為了確保您放置在隊列中的所有消息都已從隊列中取出並進行處理,並且主進程不會在此之前提前終止。 但這在您擁有的代碼中無法正常工作,因為您只給進程提供了TIMEOUT秒來處理其消息,然后如果它仍然存在並且消息可能仍留在其隊列中,則終止該進程。 這就是迫使您人為地向task_done發出額外調用的原因,這樣您在主進程中join隊列的調用就不會掛起,也是您必須首先發布此問題的原因。

因此,您可以通過兩種不同的方式進行操作。 一種方法是允許您繼續使用JoinableQueue實例並在這些實例上調用join以了解何時終止。 但是(1)您將無法過早終止您的消息進程,並且(2)您的消息進程必須正確處理異常,以便它們不會在不清空隊列的情況下過早終止。

另一種方式是我提出的,更簡單。 主進程只是在輸入隊列上放置一個特殊的標記消息,在本例中為None 這只是一條消息,不能被誤認為是要處理的實際消息,而是表示文件結束,或者換句話說,向消息進程發出一個信號,表明隊列中沒有更多消息,它可能現在終止。 因此,除了要在隊列上處理的“真實”消息之外,主進程只需要放置額外的哨兵消息,然后在消息隊列上進行join調用(現在只是常規的、不可加入的) queues),它會在每個流程實例上join(TIMEOUT) ,你會發現它不再活着,因為它已經看到了哨兵,因此你知道它已經處理了所有的消息,或者你可以在進程上調用terminate如果您願意在其輸入隊列上留下消息。

當然,要真正確定自行終止的進程確實清空了它們的隊列,可能需要您檢查它們的隊列以查看它們確實是空的。 但是我假設您應該能夠對您的流程進行編碼以正確處理異常,至少是那些可以處理的異常,以便它們不會過早終止並對每條消息執行“合理”的操作。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM