Come utilizzare la coda multiprocessing in Python?


94

Ho molti problemi a capire come funziona la coda multiprocessing su Python e come implementarla. Diciamo che ho due moduli python che accedono ai dati da un file condiviso, chiamiamo questi due moduli uno scrittore e un lettore. Il mio piano è che sia il lettore che lo scrittore inseriscano le richieste in due code multiprocessing separate, quindi un terzo processo inserisca queste richieste in un ciclo ed esegua come tale.

Il mio problema principale è che non so davvero come implementare multiprocessing.queue correttamente, non puoi davvero istanziare l'oggetto per ogni processo poiché saranno code separate, come fai ad assicurarti che tutti i processi siano correlati a una coda condivisa (o in questo caso code)


4
passare le code a ciascuna classe di processo come parametro quando vengono istanziate nel processo padre.
Joel Cornett

Risposte:


122

Il mio problema principale è che non so davvero come implementare multiprocessing.queue correttamente, non puoi davvero istanziare l'oggetto per ogni processo poiché saranno code separate, come fai ad assicurarti che tutti i processi siano correlati a una coda condivisa (o in questo caso code)

Questo è un semplice esempio di un lettore e uno scrittore che condividono una singola coda ... Lo scrittore invia un mucchio di numeri interi al lettore; quando il writer esaurisce i numeri, invia "DONE", che consente al lettore di sapere di uscire dal ciclo di lettura.

from multiprocessing import Process, Queue
import time
import sys

def reader_proc(queue):
    ## Read from the queue; this will be spawned as a separate Process
    while True:
        msg = queue.get()         # Read from the queue and do nothing
        if (msg == 'DONE'):
            break

def writer(count, queue):
    ## Write to the queue
    for ii in range(0, count):
        queue.put(ii)             # Write 'count' numbers into the queue
    queue.put('DONE')

if __name__=='__main__':
    pqueue = Queue() # writer() writes to pqueue from _this_ process
    for count in [10**4, 10**5, 10**6]:             
        ### reader_proc() reads from pqueue as a separate process
        reader_p = Process(target=reader_proc, args=((pqueue),))
        reader_p.daemon = True
        reader_p.start()        # Launch reader_proc() as a separate python process

        _start = time.time()
        writer(count, pqueue)    # Send a lot of stuff to reader()
        reader_p.join()         # Wait for the reader to finish
        print("Sending {0} numbers to Queue() took {1} seconds".format(count, 
            (time.time() - _start)))

13
Ottimo esempio. Proprio come un ulteriore bit di informazione per affrontare la confusione dell'OP ... Questo esempio mostra che una coda condivisa deve provenire dal processo master, che viene quindi passato a tutti i suoi sottoprocessi. Affinché due processi completamente non correlati condividano i dati, devono comunicare su un dispositivo di rete centrale o associato (ad esempio i socket). Qualcosa deve coordinare le informazioni.
jdi

5
bell'esempio .. anche io sono nuovo in questo argomento .. se ho più processi che eseguono la stessa funzione di destinazione (con argomenti diversi), come assicurarmi che non si scontrino mentre metto i dati nella coda .. è necessario il blocco ?
WYSIWYG

@bharat_iyengar Dalla documentazione del modulo multiprocessing, si dice che Queue è implementato usando alcuni lock / semafori. Quindi, quando usi i metodi get () e put (object) Queue, la coda si bloccherà se qualche altro processo / thread sta provando a ottenere o mettere qualcosa in coda. Quindi non devi preoccuparti di bloccarlo manualmente.
almel

1
Le condizioni di arresto esplicito sono migliori delle condizioni di arresto implicite
Mike Pennington

2
Qsize può andare a zero se i lettori della coda superano il tasso del writer della coda
Mike Pennington,

8

in " from queue import Queue" non c'è nessun modulo chiamato queue, invece multiprocessingdovrebbe essere usato. Pertanto, dovrebbe essere simile a " from multiprocessing import Queue"


11
Anche se con anni di ritardo, l'utilizzo multiprocessing.Queueè corretto. Il normale Queue.Queueviene utilizzato per i thread Python . Quando si tenta di utilizzare Queue.Queuecon l'elaborazione multipla, verranno create copie dell'oggetto Queue in ogni processo figlio e i processi figlio non verranno mai aggiornati. Fondamentalmente, Queue.Queuefunziona utilizzando un oggetto condiviso globale e multiprocessing.Queuefunziona utilizzando IPC. Vedi: stackoverflow.com/questions/925100/...
Michael Guffre

5

Ecco un utilizzo semplice e morto di multiprocessing.Queuee multiprocessing.Processche consente ai chiamanti di inviare un "evento" più argomenti a un processo separato che invia l'evento a un metodo "do_" del processo. (Python 3.4 e versioni successive)

import multiprocessing as mp
import collections

Msg = collections.namedtuple('Msg', ['event', 'args'])

class BaseProcess(mp.Process):
    """A process backed by an internal queue for simple one-way message passing.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.queue = mp.Queue()

    def send(self, event, *args):
        """Puts the event and args as a `Msg` on the queue
        """
       msg = Msg(event, args)
       self.queue.put(msg)

    def dispatch(self, msg):
        event, args = msg

        handler = getattr(self, "do_%s" % event, None)
        if not handler:
            raise NotImplementedError("Process has no handler for [%s]" % event)

        handler(*args)

    def run(self):
        while True:
            msg = self.queue.get()
            self.dispatch(msg)

Utilizzo:

class MyProcess(BaseProcess):
    def do_helloworld(self, arg1, arg2):
        print(arg1, arg2)

if __name__ == "__main__":
    process = MyProcess()
    process.start()
    process.send('helloworld', 'hello', 'world')

Il sendavviene nel processo genitore, ildo_* accade nel processo figlio.

Ho tralasciato qualsiasi gestione delle eccezioni che avrebbe ovviamente interrotto il ciclo di esecuzione e chiuso il processo figlio. Puoi anche personalizzarlo eseguendo runl' override per controllare il blocco o qualsiasi altra cosa.

Questo è davvero utile solo in situazioni in cui hai un singolo processo di lavoro, ma penso che sia una risposta pertinente a questa domanda per dimostrare uno scenario comune con un po 'più di orientamento agli oggetti.


1
Risposta eccezionale! Grazie. +50 :)
kmiklas

3

Ho dato un'occhiata a più risposte attraverso lo stack overflow e il Web mentre cercavo di impostare un modo per eseguire il multiprocessing utilizzando le code per il passaggio di frame di dati di grandi dimensioni. Mi sembrava che ogni risposta ripetesse lo stesso tipo di soluzioni senza alcuna considerazione della moltitudine di casi limite che si incontreranno sicuramente quando si impostano calcoli come questi. Il problema è che ci sono molte cose in gioco allo stesso tempo. Il numero di attività, il numero di lavoratori, la durata di ciascuna attività e le possibili eccezioni durante l'esecuzione dell'attività. Tutto ciò rende la sincronizzazione complicata e la maggior parte delle risposte non indica come puoi procedere. Quindi questa è la mia opinione dopo aver giocherellato per alcune ore, spero che sia abbastanza generico da consentire alla maggior parte delle persone di trovarlo utile.

Alcuni pensieri prima di qualsiasi esempio di codifica. Poiché queue.Emptyo queue.qsize()o qualsiasi altro metodo simile è inaffidabile per il controllo del flusso, qualsiasi codice simile

while True:
    try:
        task = pending_queue.get_nowait()
    except queue.Empty:
        break

è fasullo. Questo ucciderà il lavoratore anche se millisecondi dopo un'altra attività si presenterà nella coda. Il lavoratore non si riprenderà e dopo un po 'TUTTI i lavoratori scompariranno poiché troveranno casualmente la coda momentaneamente vuota. Il risultato finale sarà che la funzione multiprocessing principale (quella con join () sui processi) tornerà senza che tutte le attività siano state completate. Bello. Buona fortuna per eseguire il debug se hai migliaia di attività e alcune ne mancano.

L'altro problema è l'uso dei valori sentinella. Molte persone hanno suggerito di aggiungere un valore sentinella nella coda per contrassegnare la fine della coda. Ma per segnalarlo esattamente a chi? Se sono presenti N lavoratori, supponendo che N sia il numero di core disponibili dare o ricevere, un singolo valore sentinella segnalerà solo la fine della coda a un lavoratore. Tutti gli altri lavoratori si siederanno in attesa di altro lavoro quando non ne rimane nessuno. Esempi tipici che ho visto sono

while True:
    task = pending_queue.get()
    if task == SOME_SENTINEL_VALUE:
        break

Un lavoratore riceverà il valore sentinella mentre il resto aspetterà indefinitamente. Nessun post in cui mi sono imbattuto ha detto che è necessario inviare il valore sentinella alla coda ALMENO tante volte quante sono i lavoratori in modo che TUTTI lo ricevano.

L'altro problema è la gestione delle eccezioni durante l'esecuzione dell'attività. Anche in questo caso questi dovrebbero essere catturati e gestiti. Inoltre, se hai un filecompleted_tasks coda dovresti contare indipendentemente in modo deterministico quanti elementi ci sono nella coda prima di decidere che il lavoro è finito. Anche in questo caso fare affidamento sulle dimensioni della coda è destinato a fallire e restituisce risultati imprevisti.

Nell'esempio seguente, la par_proc()funzione riceverà un elenco di attività comprese le funzioni con cui queste attività dovrebbero essere eseguite insieme a qualsiasi argomento e valore con nome.

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

Ed ecco un test per eseguire il codice sopra

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

più un altro con alcune eccezioni

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

Spero che sia utile.


2

Abbiamo implementato due versioni di questo, una semplice pool multi thread che può eseguire molti tipi di invocabili, rendendo la nostra vita molto più semplice e la seconda versione che utilizza i processi , che è meno flessibile in termini di chiamabili e richiede una chiamata extra a Dill.

L'impostazione di frozen_pool su true bloccherà l'esecuzione fino a quando non verrà chiamata finish_pool_queue in una delle classi.

Versione del filo:

'''
Created on Nov 4, 2019

@author: Kevin
'''
from threading import Lock, Thread
from Queue import Queue
import traceback
from helium.loaders.loader_retailers import print_info
from time import sleep
import signal
import os

class ThreadPool(object):
    def __init__(self, queue_threads, *args, **kwargs):
        self.frozen_pool = kwargs.get('frozen_pool', False)
        self.print_queue = kwargs.get('print_queue', True)
        self.pool_results = []
        self.lock = Lock()
        self.queue_threads = queue_threads
        self.queue = Queue()
        self.threads = []

        for i in range(self.queue_threads):
            t = Thread(target=self.make_pool_call)
            t.daemon = True
            t.start()
            self.threads.append(t)

    def make_pool_call(self):
        while True:
            if self.frozen_pool:
                #print '--> Queue is frozen'
                sleep(1)
                continue

            item = self.queue.get()
            if item is None:
                break

            call = item.get('call', None)
            args = item.get('args', [])
            kwargs = item.get('kwargs', {})
            keep_results = item.get('keep_results', False)

            try:
                result = call(*args, **kwargs)

                if keep_results:
                    self.lock.acquire()
                    self.pool_results.append((item, result))
                    self.lock.release()

            except Exception as e:
                self.lock.acquire()
                print e
                traceback.print_exc()
                self.lock.release()
                os.kill(os.getpid(), signal.SIGUSR1)

            self.queue.task_done()

    def finish_pool_queue(self):
        self.frozen_pool = False

        while self.queue.unfinished_tasks > 0:
            if self.print_queue:
                print_info('--> Thread pool... %s' % self.queue.unfinished_tasks)
            sleep(5)

        self.queue.join()

        for i in range(self.queue_threads):
            self.queue.put(None)

        for t in self.threads:
            t.join()

        del self.threads[:]

    def get_pool_results(self):
        return self.pool_results

    def clear_pool_results(self):
        del self.pool_results[:]

Versione di processo:

  '''
Created on Nov 4, 2019

@author: Kevin
'''
import traceback
from helium.loaders.loader_retailers import print_info
from time import sleep
import signal
import os
from multiprocessing import Queue, Process, Value, Array, JoinableQueue, Lock,\
    RawArray, Manager
from dill import dill
import ctypes
from helium.misc.utils import ignore_exception
from mem_top import mem_top
import gc

class ProcessPool(object):
    def __init__(self, queue_processes, *args, **kwargs):
        self.frozen_pool = Value(ctypes.c_bool, kwargs.get('frozen_pool', False))
        self.print_queue = kwargs.get('print_queue', True)
        self.manager = Manager()
        self.pool_results = self.manager.list()
        self.queue_processes = queue_processes
        self.queue = JoinableQueue()
        self.processes = []

        for i in range(self.queue_processes):
            p = Process(target=self.make_pool_call)
            p.start()
            self.processes.append(p)

        print 'Processes', self.queue_processes

    def make_pool_call(self):
        while True:
            if self.frozen_pool.value:
                sleep(1)
                continue

            item_pickled = self.queue.get()

            if item_pickled is None:
                #print '--> Ending'
                self.queue.task_done()
                break

            item = dill.loads(item_pickled)

            call = item.get('call', None)
            args = item.get('args', [])
            kwargs = item.get('kwargs', {})
            keep_results = item.get('keep_results', False)

            try:
                result = call(*args, **kwargs)

                if keep_results:
                    self.pool_results.append(dill.dumps((item, result)))
                else:
                    del call, args, kwargs, keep_results, item, result

            except Exception as e:
                print e
                traceback.print_exc()
                os.kill(os.getpid(), signal.SIGUSR1)

            self.queue.task_done()

    def finish_pool_queue(self, callable=None):
        self.frozen_pool.value = False

        while self.queue._unfinished_tasks.get_value() > 0:
            if self.print_queue:
                print_info('--> Process pool... %s' % (self.queue._unfinished_tasks.get_value()))

            if callable:
                callable()

            sleep(5)

        for i in range(self.queue_processes):
            self.queue.put(None)

        self.queue.join()
        self.queue.close()

        for p in self.processes:
            with ignore_exception: p.join(10)
            with ignore_exception: p.terminate()

        with ignore_exception: del self.processes[:]

    def get_pool_results(self):
        return self.pool_results

    def clear_pool_results(self):
        del self.pool_results[:]
def test(eg):
        print 'EG', eg

Chiama con uno dei due:

tp = ThreadPool(queue_threads=2)
tp.queue.put({'call': test, 'args': [random.randint(0, 100)]})
tp.finish_pool_queue()

o

pp = ProcessPool(queue_processes=2)
pp.queue.put(dill.dumps({'call': test, 'args': [random.randint(0, 100)]}))
pp.queue.put(dill.dumps({'call': test, 'args': [random.randint(0, 100)]}))
pp.finish_pool_queue()

0

Ho appena fatto un esempio semplice e generale per dimostrare il passaggio di un messaggio su una coda tra 2 programmi autonomi. Non risponde direttamente alla domanda del PO ma dovrebbe essere abbastanza chiaro indicando il concetto.

Server:

multiprocessing-queue-manager-server.py

import asyncio
import concurrent.futures
import multiprocessing
import multiprocessing.managers
import queue
import sys
import threading
from typing import Any, AnyStr, Dict, Union


class QueueManager(multiprocessing.managers.BaseManager):

    def get_queue(self, ident: Union[AnyStr, int, type(None)] = None) -> multiprocessing.Queue:
        pass


def get_queue(ident: Union[AnyStr, int, type(None)] = None) -> multiprocessing.Queue:
    global q

    if not ident in q:
        q[ident] = multiprocessing.Queue()

    return q[ident]


q: Dict[Union[AnyStr, int, type(None)], multiprocessing.Queue] = dict()
delattr(QueueManager, 'get_queue')


def init_queue_manager_server():
    if not hasattr(QueueManager, 'get_queue'):
        QueueManager.register('get_queue', get_queue)


def serve(no: int, term_ev: threading.Event):
    manager: QueueManager
    with QueueManager(authkey=QueueManager.__name__.encode()) as manager:
        print(f"Server address {no}: {manager.address}")

        while not term_ev.is_set():
            try:
                item: Any = manager.get_queue().get(timeout=0.1)
                print(f"Client {no}: {item} from {manager.address}")
            except queue.Empty:
                continue


async def main(n: int):
    init_queue_manager_server()
    term_ev: threading.Event = threading.Event()
    executor: concurrent.futures.ThreadPoolExecutor = concurrent.futures.ThreadPoolExecutor()

    i: int
    for i in range(n):
        asyncio.ensure_future(asyncio.get_running_loop().run_in_executor(executor, serve, i, term_ev))

    # Gracefully shut down
    try:
        await asyncio.get_running_loop().create_future()
    except asyncio.CancelledError:
        term_ev.set()
        executor.shutdown()
        raise


if __name__ == '__main__':
    asyncio.run(main(int(sys.argv[1])))

Cliente:

multiprocessing-queue-manager-client.py

import multiprocessing
import multiprocessing.managers
import os
import sys
from typing import AnyStr, Union


class QueueManager(multiprocessing.managers.BaseManager):

    def get_queue(self, ident: Union[AnyStr, int, type(None)] = None) -> multiprocessing.Queue:
        pass


delattr(QueueManager, 'get_queue')


def init_queue_manager_client():
    if not hasattr(QueueManager, 'get_queue'):
        QueueManager.register('get_queue')


def main():
    init_queue_manager_client()

    manager: QueueManager = QueueManager(sys.argv[1], authkey=QueueManager.__name__.encode())
    manager.connect()

    message = f"A message from {os.getpid()}"
    print(f"Message to send: {message}")
    manager.get_queue().put(message)


if __name__ == '__main__':
    main()

Utilizzo

Server:

$ python3 multiprocessing-queue-manager-server.py N

Nè un numero intero che indica quanti server devono essere creati. Copia uno degli <server-address-N>output dal server e rendilo il primo argomento di ciascuno multiprocessing-queue-manager-client.py.

Cliente:

python3 multiprocessing-queue-manager-client.py <server-address-1>

Risultato

Server:

Client 1: <item> from <server-address-1>

Gist: https://gist.github.com/89062d639e40110c61c2f88018a8b0e5


UPD : Creato un pacchetto qui .

Server:

import ipcq


with ipcq.QueueManagerServer(address=ipcq.Address.DEFAULT, authkey=ipcq.AuthKey.DEFAULT) as server:
    server.get_queue().get()

Cliente:

import ipcq


client = ipcq.QueueManagerClient(address=ipcq.Address.DEFAULT, authkey=ipcq.AuthKey.DEFAULT)
client.get_queue().put('a message')

inserisci qui la descrizione dell'immagine

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.