Multiprocessing est un outil puissant en python, et je veux le comprendre plus en profondeur. Je veux savoir quand utiliser régulierLocks et Queues et quand utiliser un multiprocessing Manager pour partager ceux-ci parmi tous les processus.
J'ai proposé les scénarios de test suivants avec quatre conditions différentes pour le multitraitement:
Utilisation d'un pool et [~ # ~] non [~ # ~] Manager
Utilisation d'un pool et d'un gestionnaire
Utilisation de processus individuels et [~ # ~] non [~ # ~] Manager
Utilisation de processus individuels et d'un gestionnaire
Toutes les conditions exécutent une fonction de travail the_job
. the_job
Consiste en une impression sécurisée par un verrou. De plus, l'entrée de la fonction est simplement mise dans une file d'attente (pour voir si elle peut être récupérée de la file d'attente). Cette entrée est simplement un index idx
de range(10)
créé dans le script principal appelé start_scenario
(Affiché en bas).
def the_job(args):
"""The job for multiprocessing.
Prints some stuff secured by a lock and
finally puts the input into a queue.
"""
idx = args[0]
lock = args[1]
queue=args[2]
lock.acquire()
print 'I'
print 'was '
print 'here '
print '!!!!'
print '1111'
print 'einhundertelfzigelf\n'
who= ' By run %d \n' % idx
print who
lock.release()
queue.put(idx)
Le succès d'une condition est défini comme rappelant parfaitement l'entrée de la file d'attente, voir la fonction read_queue
En bas.
Les conditions 1 et 2 sont assez explicites. La condition 1 implique la création d'un verrou et d'une file d'attente, et leur transmission à un pool de processus:
def scenario_1_pool_no_manager(jobfunc, args, ncores):
"""Runs a pool of processes WITHOUT a Manager for the lock and queue.
FAILS!
"""
mypool = mp.Pool(ncores)
lock = mp.Lock()
queue = mp.Queue()
iterator = make_iterator(args, lock, queue)
mypool.imap(jobfunc, iterator)
mypool.close()
mypool.join()
return read_queue(queue)
(La fonction d'aide make_iterator
Est donnée au bas de cet article.) La condition 1 échoue avec RuntimeError: Lock objects should only be shared between processes through inheritance
.
La condition 2 est assez similaire mais maintenant le verrou et la file d'attente sont sous la supervision d'un gestionnaire:
def scenario_2_pool_manager(jobfunc, args, ncores):
"""Runs a pool of processes WITH a Manager for the lock and queue.
SUCCESSFUL!
"""
mypool = mp.Pool(ncores)
lock = mp.Manager().Lock()
queue = mp.Manager().Queue()
iterator = make_iterator(args, lock, queue)
mypool.imap(jobfunc, iterator)
mypool.close()
mypool.join()
return read_queue(queue)
Dans la condition 3, de nouveaux processus sont démarrés manuellement et le verrou et la file d'attente sont créés sans gestionnaire:
def scenario_3_single_processes_no_manager(jobfunc, args, ncores):
"""Runs an individual process for every task WITHOUT a Manager,
SUCCESSFUL!
"""
lock = mp.Lock()
queue = mp.Queue()
iterator = make_iterator(args, lock, queue)
do_job_single_processes(jobfunc, iterator, ncores)
return read_queue(queue)
La condition 4 est similaire mais utilise à nouveau un gestionnaire:
def scenario_4_single_processes_manager(jobfunc, args, ncores):
"""Runs an individual process for every task WITH a Manager,
SUCCESSFUL!
"""
lock = mp.Manager().Lock()
queue = mp.Manager().Queue()
iterator = make_iterator(args, lock, queue)
do_job_single_processes(jobfunc, iterator, ncores)
return read_queue(queue)
Dans les deux conditions - 3 et 4 - je lance un nouveau processus pour chacune des 10 tâches de the_job
Avec au plus ncores processus fonctionnant en même temps. Ceci est réalisé avec la fonction d'assistance suivante:
def do_job_single_processes(jobfunc, iterator, ncores):
"""Runs a job function by starting individual processes for every task.
At most `ncores` processes operate at the same time
:param jobfunc: Job to do
:param iterator:
Iterator over different parameter settings,
contains a lock and a queue
:param ncores:
Number of processes operating at the same time
"""
keep_running=True
process_dict = {} # Dict containing all subprocees
while len(process_dict)>0 or keep_running:
terminated_procs_pids = []
# First check if some processes did finish their job
for pid, proc in process_dict.iteritems():
# Remember the terminated processes
if not proc.is_alive():
terminated_procs_pids.append(pid)
# And delete these from the process dict
for terminated_proc in terminated_procs_pids:
process_dict.pop(terminated_proc)
# If we have less active processes than ncores and there is still
# a job to do, add another process
if len(process_dict) < ncores and keep_running:
try:
task = iterator.next()
proc = mp.Process(target=jobfunc,
args=(task,))
proc.start()
process_dict[proc.pid]=proc
except StopIteration:
# All tasks have been started
keep_running=False
time.sleep(0.1)
Seule la condition 1 échoue (RuntimeError: Lock objects should only be shared between processes through inheritance
) Alors que les 3 autres conditions sont réussies. J'essaie de comprendre ce résultat.
Pourquoi le pool doit-il partager un verrou et une file d'attente entre tous les processus, mais pas les processus individuels de la condition 3?
Ce que je sais, c'est que pour les conditions de pool (1 et 2), toutes les données des itérateurs sont transmises via le décapage, tandis que dans les conditions de processus unique (3 et 4), toutes les données des itérateurs sont transmises par héritage du processus principal (je suis en utilisant Linux ). Je suppose que jusqu'à ce que la mémoire soit modifiée à partir d'un processus enfant, la même mémoire que le processus parental utilise est accessible (copie sur écriture). Mais dès que l'on dit lock.acquire()
, cela devrait être changé et les processus enfants utilisent des verrous différents placés ailleurs en mémoire, n'est-ce pas? Comment un processus enfant sait-il qu'un frère a activé un verrou qui n'est pas partagé via un gestionnaire?
Enfin, ma question est quelque peu liée à la différence des conditions 3 et 4. Les deux ont des processus individuels, mais ils diffèrent dans l'utilisation d'un gestionnaire. Les deux sont-ils considérés comme du code valide? Ou faut-il éviter d'utiliser un gestionnaire s'il n'en a pas réellement besoin?
Pour ceux qui veulent simplement copier et coller tout pour exécuter le code, voici le script complet:
__author__ = 'Me and myself'
import multiprocessing as mp
import time
def the_job(args):
"""The job for multiprocessing.
Prints some stuff secured by a lock and
finally puts the input into a queue.
"""
idx = args[0]
lock = args[1]
queue=args[2]
lock.acquire()
print 'I'
print 'was '
print 'here '
print '!!!!'
print '1111'
print 'einhundertelfzigelf\n'
who= ' By run %d \n' % idx
print who
lock.release()
queue.put(idx)
def read_queue(queue):
"""Turns a qeue into a normal python list."""
results = []
while not queue.empty():
result = queue.get()
results.append(result)
return results
def make_iterator(args, lock, queue):
"""Makes an iterator over args and passes the lock an queue to each element."""
return ((arg, lock, queue) for arg in args)
def start_scenario(scenario_number = 1):
"""Starts one of four multiprocessing scenarios.
:param scenario_number: Index of scenario, 1 to 4
"""
args = range(10)
ncores = 3
if scenario_number==1:
result = scenario_1_pool_no_manager(the_job, args, ncores)
Elif scenario_number==2:
result = scenario_2_pool_manager(the_job, args, ncores)
Elif scenario_number==3:
result = scenario_3_single_processes_no_manager(the_job, args, ncores)
Elif scenario_number==4:
result = scenario_4_single_processes_manager(the_job, args, ncores)
if result != args:
print 'Scenario %d fails: %s != %s' % (scenario_number, args, result)
else:
print 'Scenario %d successful!' % scenario_number
def scenario_1_pool_no_manager(jobfunc, args, ncores):
"""Runs a pool of processes WITHOUT a Manager for the lock and queue.
FAILS!
"""
mypool = mp.Pool(ncores)
lock = mp.Lock()
queue = mp.Queue()
iterator = make_iterator(args, lock, queue)
mypool.map(jobfunc, iterator)
mypool.close()
mypool.join()
return read_queue(queue)
def scenario_2_pool_manager(jobfunc, args, ncores):
"""Runs a pool of processes WITH a Manager for the lock and queue.
SUCCESSFUL!
"""
mypool = mp.Pool(ncores)
lock = mp.Manager().Lock()
queue = mp.Manager().Queue()
iterator = make_iterator(args, lock, queue)
mypool.map(jobfunc, iterator)
mypool.close()
mypool.join()
return read_queue(queue)
def scenario_3_single_processes_no_manager(jobfunc, args, ncores):
"""Runs an individual process for every task WITHOUT a Manager,
SUCCESSFUL!
"""
lock = mp.Lock()
queue = mp.Queue()
iterator = make_iterator(args, lock, queue)
do_job_single_processes(jobfunc, iterator, ncores)
return read_queue(queue)
def scenario_4_single_processes_manager(jobfunc, args, ncores):
"""Runs an individual process for every task WITH a Manager,
SUCCESSFUL!
"""
lock = mp.Manager().Lock()
queue = mp.Manager().Queue()
iterator = make_iterator(args, lock, queue)
do_job_single_processes(jobfunc, iterator, ncores)
return read_queue(queue)
def do_job_single_processes(jobfunc, iterator, ncores):
"""Runs a job function by starting individual processes for every task.
At most `ncores` processes operate at the same time
:param jobfunc: Job to do
:param iterator:
Iterator over different parameter settings,
contains a lock and a queue
:param ncores:
Number of processes operating at the same time
"""
keep_running=True
process_dict = {} # Dict containing all subprocees
while len(process_dict)>0 or keep_running:
terminated_procs_pids = []
# First check if some processes did finish their job
for pid, proc in process_dict.iteritems():
# Remember the terminated processes
if not proc.is_alive():
terminated_procs_pids.append(pid)
# And delete these from the process dict
for terminated_proc in terminated_procs_pids:
process_dict.pop(terminated_proc)
# If we have less active processes than ncores and there is still
# a job to do, add another process
if len(process_dict) < ncores and keep_running:
try:
task = iterator.next()
proc = mp.Process(target=jobfunc,
args=(task,))
proc.start()
process_dict[proc.pid]=proc
except StopIteration:
# All tasks have been started
keep_running=False
time.sleep(0.1)
def main():
"""Runs 1 out of 4 different multiprocessing scenarios"""
start_scenario(1)
if __name__ == '__main__':
main()
multiprocessing.Lock
Est implémenté à l'aide d'un objet Semaphore fourni par le système d'exploitation. Sous Linux, l'enfant hérite simplement d'un descripteur du sémaphore du parent via os.fork
. Ce n'est pas une copie du sémaphore; il hérite en fait du même handle que le parent, de la même manière que les descripteurs de fichiers peuvent être hérités. Windows d'autre part, ne prend pas en charge os.fork
, Il doit donc décaper le Lock
. Pour ce faire, il crée un descripteur en double du sémaphore Windows utilisé en interne par l'objet multiprocessing.Lock
, À l'aide de l'API Windows DuplicateHandle
, qui indique:
La poignée en double fait référence au même objet que la poignée d'origine. Par conséquent, toutes les modifications apportées à l'objet sont reflétées par les deux poignées
L'API DuplicateHandle
vous permet de donner la propriété du handle dupliqué au processus enfant, afin que le processus enfant puisse réellement l'utiliser après l'avoir décroché. En créant une poignée dupliquée appartenant à l'enfant, vous pouvez effectivement "partager" l'objet verrou.
Voici l'objet sémaphore dans multiprocessing/synchronize.py
class SemLock(object):
def __init__(self, kind, value, maxvalue):
sl = self._semlock = _multiprocessing.SemLock(kind, value, maxvalue)
debug('created semlock with handle %s' % sl.handle)
self._make_methods()
if sys.platform != 'win32':
def _after_fork(obj):
obj._semlock._after_fork()
register_after_fork(self, _after_fork)
def _make_methods(self):
self.acquire = self._semlock.acquire
self.release = self._semlock.release
self.__enter__ = self._semlock.__enter__
self.__exit__ = self._semlock.__exit__
def __getstate__(self): # This is called when you try to pickle the `Lock`.
assert_spawning(self)
sl = self._semlock
return (Popen.duplicate_for_child(sl.handle), sl.kind, sl.maxvalue)
def __setstate__(self, state): # This is called when unpickling a `Lock`
self._semlock = _multiprocessing.SemLock._rebuild(*state)
debug('recreated blocker with handle %r' % state[0])
self._make_methods()
Notez l'appel assert_spawning
Dans __getstate__
, Qui est appelé lors du décapage de l'objet. Voici comment cela est mis en œuvre:
#
# Check that the current thread is spawning a child process
#
def assert_spawning(self):
if not Popen.thread_is_spawning():
raise RuntimeError(
'%s objects should only be shared between processes'
' through inheritance' % type(self).__name__
)
Cette fonction est celle qui garantit que vous "héritez" du Lock
, en appelant thread_is_spawning
. Sous Linux, cette méthode renvoie simplement False
:
@staticmethod
def thread_is_spawning():
return False
C'est parce que Linux n'a pas besoin de décaper pour hériter Lock
, donc si __getstate__
Est réellement appelé sur Linux, nous ne devons pas hériter. Sous Windows, il se passe plus:
def dump(obj, file, protocol=None):
ForkingPickler(file, protocol).dump(obj)
class Popen(object):
'''
Start a subprocess to run the code of a process object
'''
_tls = thread._local()
def __init__(self, process_obj):
...
# send information to child
prep_data = get_preparation_data(process_obj._name)
to_child = os.fdopen(wfd, 'wb')
Popen._tls.process_handle = int(hp)
try:
dump(prep_data, to_child, HIGHEST_PROTOCOL)
dump(process_obj, to_child, HIGHEST_PROTOCOL)
finally:
del Popen._tls.process_handle
to_child.close()
@staticmethod
def thread_is_spawning():
return getattr(Popen._tls, 'process_handle', None) is not None
Ici, thread_is_spawning
Renvoie True
si l'objet Popen._tls
A un attribut process_handle
. Nous pouvons voir que l'attribut process_handle
Est créé dans __init__
, Puis les données dont nous voulons hériter sont transmises du parent à l'enfant en utilisant dump
, puis l'attribut est supprimé. Ainsi, thread_is_spawning
Ne sera True
que pendant __init__
. Selon ce fil de liste de diffusion python-ideas , il s'agit en fait d'une limitation artificielle ajoutée pour simuler le même comportement que os.fork
Sous Linux. Windows pourrait prendre en charge le passage de Lock
à tout moment, car DuplicateHandle
peut être exécuté à tout moment.
Tout ce qui précède s'applique à l'objet Queue
car il utilise Lock
en interne.
Je dirais que l'héritage des objets Lock
est préférable à l'utilisation d'une Manager.Lock()
, car lorsque vous utilisez un Manager.Lock
, Chaque appel que vous effectuez vers le Lock
doit être envoyé via IPC au processus Manager
, ce qui sera beaucoup plus lent que d'utiliser un Lock
partagé qui vit à l'intérieur du processus appelant. Les deux approches sont parfaitement valides, cependant.
Enfin, il est possible de passer un Lock
à tous les membres d'un Pool
sans utiliser un Manager
, en utilisant le initializer
/initargs
arguments de mots clés:
lock = None
def initialize_lock(l):
global lock
lock = l
def scenario_1_pool_no_manager(jobfunc, args, ncores):
"""Runs a pool of processes WITHOUT a Manager for the lock and queue.
"""
lock = mp.Lock()
mypool = mp.Pool(ncores, initializer=initialize_lock, initargs=(lock,))
queue = mp.Queue()
iterator = make_iterator(args, queue)
mypool.imap(jobfunc, iterator) # Don't pass lock. It has to be used as a global in the child. (This means `jobfunc` would need to be re-written slightly.
mypool.close()
mypool.join()
return read_queue(queue)
Cela fonctionne car les arguments passés à initargs
sont passés à la méthode __init__
Des objets Process
qui s'exécutent à l'intérieur de Pool
, donc ils finissent par être hérités, plutôt que marinés.