web-dev-qa-db-ja.com

マルチプロセッシングについて:Pythonの共有メモリ管理、ロック、キュー

Multiprocessing はPythonの強力なツールであり、より深く理解したいと思います。 regularLocks および Queues を使用するタイミングと、マルチプロセッシングを使用するタイミングを知りたい Manager =すべてのプロセスでこれらを共有します。

マルチプロセッシングの4つの異なる条件で、次のテストシナリオを思いつきました。

  1. プールと[〜#〜] no [〜#〜]Managerの使用

  2. プールとマネージャーの使用

  3. 個々のプロセスと[〜#〜] no [〜#〜]Managerの使用

  4. 個々のプロセスとマネージャーを使用する

仕事

すべての条件がジョブ関数_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()
_
38
SmCaterpillar

_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___メソッドに渡されるため、ピクルスではなく継承されるために機能します。

31
dano