Multiprocessing はPythonの強力なツールであり、より深く理解したいと思います。 regularLocks および Queues を使用するタイミングと、マルチプロセッシングを使用するタイミングを知りたい Manager =すべてのプロセスでこれらを共有します。
マルチプロセッシングの4つの異なる条件で、次のテストシナリオを思いつきました。
プールと[〜#〜] no [〜#〜]Managerの使用
プールとマネージャーの使用
個々のプロセスと[〜#〜] no [〜#〜]Managerの使用
個々のプロセスとマネージャーを使用する
すべての条件がジョブ関数_the_job
_を実行します。 _the_job
_は、ロックで保護された印刷で構成されています。さらに、関数への入力は単純にキューに入れられます(キューから回復できるかどうかを確認するため)。この入力は、_start_scenario
_と呼ばれるメインスクリプトで作成されたrange(10)
からのインデックスidx
(下部に表示)です。
_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)
_
条件の成功は、キューからの入力を完全にリコールすることとして定義されます。下部の関数_read_queue
_を参照してください。
条件1と2は、むしろ自明です。条件1では、ロックとキューを作成し、これらをプロセスプールに渡します。
_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)
_
(ヘルパー関数_make_iterator
_はこの投稿の最後にあります。)条件1は_RuntimeError: Lock objects should only be shared between processes through inheritance
_で失敗します。
条件2はかなり似ていますが、ロックとキューはマネージャーの監督下にあります。
_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)
_
条件3では、新しいプロセスが手動で開始され、マネージャーなしでロックとキューが作成されます。
_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)
_
条件4も同様ですが、今度はマネージャーを使用しています:
_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)
_
両方の条件-3と4-では、_the_job
_の10個のタスクごとに、最大でncoresプロセスが同時に動作する新しいプロセスを開始します。これは、次のヘルパー関数で実現されます。
_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)
_
条件1のみが失敗し(_RuntimeError: Lock objects should only be shared between processes through inheritance
_)、他の3つの条件は成功します。私はこの結果に頭を包み込もうとします。
プールはすべてのプロセス間でロックとキューを共有する必要があるのに、条件3の個々のプロセスは共有しないのはなぜですか?
私が知っていることは、プール条件(1および2)ではイテレータからのすべてのデータが酸洗いを介して渡されるのに対し、単一プロセス条件(3および4)ではイテレータからのすべてのデータがメインプロセスからの継承(私は午前Linux)を使用します。子プロセス内からメモリが変更されるまで、親プロセスが使用するのと同じメモリがアクセスされます(コピーオンライト)。しかし、lock.acquire()
と言ったらすぐにこれを変更する必要があり、子プロセスはメモリ内の別の場所にある別のロックを使用します。 1つの子プロセスは、兄弟がマネージャーを介して共有されていないロックをアクティブにしたことをどのように知るのですか?
最後に、条件3と4がどれほど異なるかという私の質問が多少関連しています。両方に個別のプロセスがありますが、マネージャーの使用法が異なります。両方ともvalidコードと見なされますか?または、実際にマネージャーが必要ない場合は、マネージャーの使用を避ける必要がありますか?
コードを実行するためにすべてを単にコピーアンドペーストしたい人のために、完全なスクリプトを以下に示します。
___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 __== '__main__':
main()
_
_multiprocessing.Lock
_は、OSが提供するセマフォオブジェクトを使用して実装されます。 Linuxでは、子は_os.fork
_を介して親からセマフォへのハンドルを継承します。これはセマフォのコピーではありません。実際には、ファイル記述子を継承できるのと同じ方法で、親が持っているのと同じハンドルを継承しています。一方、Windowsは_os.fork
_をサポートしていないため、Lock
をピクルスする必要があります。これは、Windows DuplicateHandle
APIを使用して、_multiprocessing.Lock
_オブジェクトによって内部的に使用されるWindowsセマフォへの複製ハンドルを作成することによりこれを行います。
複製ハンドルは、元のハンドルと同じオブジェクトを参照します。したがって、オブジェクトへの変更は両方のハンドルを介して反映されます
DuplicateHandle
APIを使用すると、複製されたハンドルの所有権を子プロセスに与えることができるため、子プロセスは、ピックを解除した後に実際に使用できます。子が所有する複製ハンドルを作成することにより、ロックオブジェクトを効果的に「共有」できます。
_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()
_
_assert_spawning
_の___getstate__
_呼び出しに注意してください。この呼び出しは、オブジェクトを酸洗いするときに呼び出されます。実装方法は次のとおりです。
_#
# 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__
)
_
その関数は、_thread_is_spawning
_を呼び出すことで、Lock
を「継承」していることを確認するものです。 Linuxでは、そのメソッドはFalse
を返すだけです。
_@staticmethod
def thread_is_spawning():
return False
_
これは、LinuxがLock
を継承するためにpickleする必要がないため、実際にLinuxで___getstate__
_が呼び出されている場合、継承してはいけません。 Windowsでは、さらに多くのことが行われています。
_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
_
ここで、_thread_is_spawning
_オブジェクトに_Popen._tls
_属性がある場合、_process_handle
_はTrue
を返します。 _process_handle
_属性が___init__
_で作成され、継承したいデータがdump
を使用して親から子に渡され、属性が削除されることがわかります。したがって、_thread_is_spawning
_は、___init__
_の間のみTrue
になります。 このpython-ideasメーリングリストスレッド によると、これは実際にはLinuxの_os.fork
_と同じ動作をシミュレートするために追加された人為的な制限です。実際には、WindowsはcouldLock
をいつでも実行できるため、DuplicateHandle
の受け渡しをいつでもサポートしています。
上記のすべては、Queue
を内部的に使用するため、Lock
オブジェクトに適用されます。
Lock
オブジェクトを継承することは、Manager.Lock()
を使用するよりも望ましいと言えます。なぜなら、_Manager.Lock
_を使用する場合、Lock
を呼び出すたびにIPC Manager
プロセスに対して、呼び出しプロセス内にある共有Lock
を使用するよりもはるかに遅くなりますが、両方のアプローチは完全に有効です。
最後に、Lock
/Pool
キーワード引数を使用して、Manager
を使用せずに、initializer
のすべてのメンバーにinitargs
を渡すことができます。
_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)
_
これは、initargs
に渡される引数が、Process
内で実行されるPool
オブジェクトの___init__
_メソッドに渡されるため、ピクルスではなく継承されるために機能します。