Многопроцессорность Python и обработка исключений у работников

Я использую библиотеку многопроцессорности python для алгоритма, в котором у меня много рабочих, обрабатывающих определенные данные и возвращающие результат в родительский процесс. Я использую multiprocessing.Queue для передачи заданий рабочим, а второй для сбора результатов.

Все работает очень хорошо, пока работник не обработает некоторый фрагмент данных. В упрощенном примере ниже каждый рабочий имеет две фазы:

  • инициализация - может завершиться неудачей, в этом случае работник должен быть уничтожен
  • обработка данных - обработка фрагмента данных может завершиться неудачей, в этом случае работник должен пропустить этот фрагмент и продолжить со следующими данными.

Когда одна из этих фаз выходит из строя, я заканчиваю завершение после завершения script. Этот код имитирует мою проблему:

import multiprocessing as mp
import random

workers_count = 5
# Probability of failure, change to simulate failures
fail_init_p = 0.2
fail_job_p = 0.3


#========= Worker =========
def do_work(job_state, arg):
    if random.random() < fail_job_p:
        raise Exception("Job failed")
    return "job %d processed %d" % (job_state, arg)

def init(args):
    if random.random() < fail_init_p:
        raise Exception("Worker init failed")
    return args

def worker_function(args, jobs_queue, result_queue):
    # INIT
    # What to do when init() fails?
    try:
        state = init(args)
    except:
        print "!Worker %d init fail" % args
        return
    # DO WORK
    # Process data in the jobs queue
    for job in iter(jobs_queue.get, None):
        try:
            # Can throw an exception!
            result = do_work(state, job)
            result_queue.put(result)
        except:
            print "!Job %d failed, skip..." % job
        finally:
            jobs_queue.task_done()
    # Telling that we are done with processing stop token
    jobs_queue.task_done()



#========= Parent =========
jobs = mp.JoinableQueue()
results = mp.Queue()
for i in range(workers_count):
    mp.Process(target=worker_function, args=(i, jobs, results)).start()

# Populate jobs queue
results_to_expect = 0
for j in range(30):
    jobs.put(j)
    results_to_expect += 1

# Collecting the results
# What if some workers failed to process the job and we have
# less results than expected
for r in range(results_to_expect):
    result = results.get()
    print result

#Signal all workers to finish
for i in range(workers_count):
    jobs.put(None)

#Wait for them to finish
jobs.join()

У меня есть два вопроса об этом коде:

  • Когда init() выходит из строя, как обнаружить, что рабочий недействителен и не дожидаться завершения?
  • Когда do_work() выходит из строя, как уведомить родительский процесс о том, что в очереди результатов следует ожидать меньше результатов?

Благодарим вас за помощь!

Ответ 1

Я немного изменил код, чтобы он работал (см. объяснение ниже).

import multiprocessing as mp
import random

workers_count = 5
# Probability of failure, change to simulate failures
fail_init_p = 0.5
fail_job_p = 0.4


#========= Worker =========
def do_work(job_state, arg):
    if random.random() < fail_job_p:
        raise Exception("Job failed")
    return "job %d processed %d" % (job_state, arg)

def init(args):
    if random.random() < fail_init_p:
        raise Exception("Worker init failed")
    return args

def worker_function(args, jobs_queue, result_queue):
    # INIT
    # What to do when init() fails?
    try:
        state = init(args)
    except:
        print "!Worker %d init fail" % args
        result_queue.put('init failed')
        return
    # DO WORK
    # Process data in the jobs queue
    for job in iter(jobs_queue.get, None):
        try:
            # Can throw an exception!
            result = do_work(state, job)
            result_queue.put(result)
        except:
            print "!Job %d failed, skip..." % job
            result_queue.put('job failed')


#========= Parent =========
jobs = mp.Queue()
results = mp.Queue()
for i in range(workers_count):
    mp.Process(target=worker_function, args=(i, jobs, results)).start()

# Populate jobs queue
results_to_expect = 0
for j in range(30):
    jobs.put(j)
    results_to_expect += 1

init_failures = 0
job_failures = 0
successes = 0
while job_failures + successes < 30 and init_failures < workers_count:
    result = results.get()
    init_failures += int(result == 'init failed')
    job_failures += int(result == 'job failed')
    successes += int(result != 'init failed' and result != 'job failed')
    #print init_failures, job_failures, successes

for ii in range(workers_count):
    jobs.put(None)

Мои изменения:

  • Изменен jobs как обычный Queue (вместо JoinableQueue).
  • Рабочие теперь связывают специальные строки результатов "init failed" и "job failed".
  • Мастер-процесс контролирует указанные специальные результаты, пока действуют определенные условия.
  • В конце концов, поставьте "стоп" запросы (т.е. None заданий) для любого количества рабочих, которых у вас есть, независимо. Обратите внимание, что не все из них могут быть вытащены из очереди (в случае, если работник не смог инициализировать).

Кстати, ваш оригинальный код был приятным и легким в работе. Бит случайных вероятностей довольно крут.