Ich habe mir mehrere Antworten über den Stapelüberlauf und das Web hinweg angesehen, während ich versucht habe, eine Methode für die Mehrfachverarbeitung mithilfe von Warteschlangen für die Weitergabe großer Pandas-Datenrahmen einzurichten. Es schien mir, dass jede Antwort die gleiche Art von Lösungen wiederholte, ohne die Vielzahl von Randfällen zu berücksichtigen, auf die man bei der Erstellung solcher Berechnungen definitiv stoßen wird. Das Problem ist, dass viele Dinge gleichzeitig im Spiel sind. Die Anzahl der Aufgaben, die Anzahl der Mitarbeiter, die Dauer jeder Aufgabe und mögliche Ausnahmen während der Ausführung der Aufgabe. All dies macht die Synchronisation schwierig und die meisten Antworten beziehen sich nicht darauf, wie Sie vorgehen können. Das ist also meine Einstellung, nachdem ich ein paar Stunden herumgespielt habe. Hoffentlich ist dies allgemein genug, damit die meisten Leute es nützlich finden.
Einige Gedanken vor Codierungsbeispielen. Da queue.Empty
oder queue.qsize()
oder ein anderes ähnliches Verfahren für die Flusskontrolle unzuverlässig ist, kann jeder Code dergleichen verwendet werden
while True:
try:
task = pending_queue.get_nowait()
except queue.Empty:
break
ist falsch. Dies wird den Arbeiter töten, selbst wenn Millisekunden später eine andere Aufgabe in der Warteschlange auftaucht. Der Arbeiter wird sich nicht erholen und nach einer Weile verschwinden ALLE Arbeiter, da sie die Warteschlange zufällig für einen Moment leer finden. Das Endergebnis ist, dass die Haupt-Multiprozessor-Funktion (die mit dem Join () für die Prozesse) zurückgegeben wird, ohne dass alle Aufgaben abgeschlossen sind. Nett. Viel Glück beim Debuggen, wenn Sie Tausende von Aufgaben haben und einige fehlen.
Das andere Problem ist die Verwendung von Sentinel-Werten. Viele Leute haben vorgeschlagen, einen Sentinel-Wert in die Warteschlange aufzunehmen, um das Ende der Warteschlange zu kennzeichnen. Aber um es genau wem zu kennzeichnen? Wenn es N Worker gibt, unter der Annahme, dass N die Anzahl der verfügbaren Kerne ist, die geben oder nehmen, markiert ein einzelner Sentinel-Wert nur das Ende der Warteschlange für einen Worker. Alle anderen Arbeiter werden sitzen und auf weitere Arbeit warten, wenn keine mehr übrig ist. Typische Beispiele, die ich gesehen habe, sind
while True:
task = pending_queue.get()
if task == SOME_SENTINEL_VALUE:
break
Ein Mitarbeiter erhält den Sentinel-Wert, während der Rest auf unbestimmte Zeit wartet. In keinem Beitrag, auf den ich gestoßen bin, wurde erwähnt, dass Sie den Sentinel-Wert mindestens so oft an die Warteschlange senden müssen, wie Sie Mitarbeiter haben, damit ALLE ihn erhalten.
Das andere Problem ist die Behandlung von Ausnahmen während der Taskausführung. Auch diese sollten gefangen und verwaltet werden. Wenn Sie eine completed_tasks
Warteschlange haben, sollten Sie außerdem unabhängig und deterministisch zählen, wie viele Elemente sich in der Warteschlange befinden, bevor Sie entscheiden, dass die Aufgabe erledigt ist. Das Verlassen auf Warteschlangengrößen schlägt erneut fehl und gibt unerwartete Ergebnisse zurück.
Im folgenden Beispiel par_proc()
erhält die Funktion eine Liste von Aufgaben, einschließlich der Funktionen, mit denen diese Aufgaben ausgeführt werden sollen, sowie benannte Argumente und Werte.
import multiprocessing as mp
import dill as pickle
import queue
import time
import psutil
SENTINEL = None
def do_work(tasks_pending, tasks_completed):
# Get the current worker's name
worker_name = mp.current_process().name
while True:
try:
task = tasks_pending.get_nowait()
except queue.Empty:
print(worker_name + ' found an empty queue. Sleeping for a while before checking again...')
time.sleep(0.01)
else:
try:
if task == SENTINEL:
print(worker_name + ' no more work left to be done. Exiting...')
break
print(worker_name + ' received some work... ')
time_start = time.perf_counter()
work_func = pickle.loads(task['func'])
result = work_func(**task['task'])
tasks_completed.put({work_func.__name__: result})
time_end = time.perf_counter() - time_start
print(worker_name + ' done in {} seconds'.format(round(time_end, 5)))
except Exception as e:
print(worker_name + ' task failed. ' + str(e))
tasks_completed.put({work_func.__name__: None})
def par_proc(job_list, num_cpus=None):
# Get the number of cores
if not num_cpus:
num_cpus = psutil.cpu_count(logical=False)
print('* Parallel processing')
print('* Running on {} cores'.format(num_cpus))
# Set-up the queues for sending and receiving data to/from the workers
tasks_pending = mp.Queue()
tasks_completed = mp.Queue()
# Gather processes and results here
processes = []
results = []
# Count tasks
num_tasks = 0
# Add the tasks to the queue
for job in job_list:
for task in job['tasks']:
expanded_job = {}
num_tasks = num_tasks + 1
expanded_job.update({'func': pickle.dumps(job['func'])})
expanded_job.update({'task': task})
tasks_pending.put(expanded_job)
# Use as many workers as there are cores (usually chokes the system so better use less)
num_workers = num_cpus
# We need as many sentinels as there are worker processes so that ALL processes exit when there is no more
# work left to be done.
for c in range(num_workers):
tasks_pending.put(SENTINEL)
print('* Number of tasks: {}'.format(num_tasks))
# Set-up and start the workers
for c in range(num_workers):
p = mp.Process(target=do_work, args=(tasks_pending, tasks_completed))
p.name = 'worker' + str(c)
processes.append(p)
p.start()
# Gather the results
completed_tasks_counter = 0
while completed_tasks_counter < num_tasks:
results.append(tasks_completed.get())
completed_tasks_counter = completed_tasks_counter + 1
for p in processes:
p.join()
return results
Und hier ist ein Test, gegen den der obige Code ausgeführt werden kann
def test_parallel_processing():
def heavy_duty1(arg1, arg2, arg3):
return arg1 + arg2 + arg3
def heavy_duty2(arg1, arg2, arg3):
return arg1 * arg2 * arg3
task_list = [
{'func': heavy_duty1, 'tasks': [{'arg1': 1, 'arg2': 2, 'arg3': 3}, {'arg1': 1, 'arg2': 3, 'arg3': 5}]},
{'func': heavy_duty2, 'tasks': [{'arg1': 1, 'arg2': 2, 'arg3': 3}, {'arg1': 1, 'arg2': 3, 'arg3': 5}]},
]
results = par_proc(task_list)
job1 = sum([y for x in results if 'heavy_duty1' in x.keys() for y in list(x.values())])
job2 = sum([y for x in results if 'heavy_duty2' in x.keys() for y in list(x.values())])
assert job1 == 15
assert job2 == 21
plus eine andere mit einigen Ausnahmen
def test_parallel_processing_exceptions():
def heavy_duty1_raises(arg1, arg2, arg3):
raise ValueError('Exception raised')
return arg1 + arg2 + arg3
def heavy_duty2(arg1, arg2, arg3):
return arg1 * arg2 * arg3
task_list = [
{'func': heavy_duty1_raises, 'tasks': [{'arg1': 1, 'arg2': 2, 'arg3': 3}, {'arg1': 1, 'arg2': 3, 'arg3': 5}]},
{'func': heavy_duty2, 'tasks': [{'arg1': 1, 'arg2': 2, 'arg3': 3}, {'arg1': 1, 'arg2': 3, 'arg3': 5}]},
]
results = par_proc(task_list)
job1 = sum([y for x in results if 'heavy_duty1' in x.keys() for y in list(x.values())])
job2 = sum([y for x in results if 'heavy_duty2' in x.keys() for y in list(x.values())])
assert not job1
assert job2 == 21
Hoffe das ist hilfreich.