web-dev-qa-db-ja.com

マルチプロセッシングapply_asyncでインスタンスの状態を維持する

インスタンスメソッドでapply_asyncを呼び出してその結果を取得する場合、行われた変更は、フォークされたプロセスの一部のままであることを期待していました。ただし、apply_asyncを呼び出すたびに、そのインスタンスの新しいコピーが作成されるようです。

次のコードを見てください。

from multiprocessing.pool import Pool


class Multitest:
    def __init__(self):
        self.i = 0

    def run(self):
        with Pool(2) as pool:
            worker_jobs = []
            for j in range(10):
                job = pool.apply_async(self.process, (j,))
                worker_jobs.append(job)

            for job in worker_jobs:
                res = job.get()
                print("input", res)

    def process(self, inp):
        print("i", self.i)
        self.i += 1

        return inp

if __name__ == '__main__':
    mt = Multitest()
    mt.run()

出力例:

i 0
i 0
i 0
i 0
i 0
input 0
i 0
i 0
i 0
i 0
i 0
input 1
input 2
input 3
input 4
input 5
input 6
input 7
input 8
input 9

しかし、10個の入力が分散される2つのコアがあるため、iプロパティが増加することを期待していました。

私は次のフローを期待していました:

  • メインスレッドがインスタンスを作成し、run()を呼び出します
  • メインスレッドは、2つの新しいプロセスと元のMultitestインスタンスのコピー(apply_async)を初期化することにより、i = 0の作業をプールに分散します。
  • process()が新しいプロセスで何度も呼び出されます(range()が使い果たされるまで)。プロセスを呼び出すたびに、そのプロセスのself.iがインクリメントされます

:私はnotで、2つのプロセス間で共有状態を尋ねています。代わりに、単一のプロセスのクラスインスタンスが変更されない理由を尋ねています(なぜ、個々のプロセスのself.iがインクリメントされないのですか)。

しかし、私はこの行動を見ていません。代わりに、出力された出力はゼロのみで、私の期待が間違っていたことを示しています。状態(プロパティi)は維持されませんが、apply_asyncを呼び出すたびに新しいインスタンス(または少なくとも新しいコピー)が作成されます。ここで何が欠けていますか、そしてこれを期待どおりに機能させるにはどうすればよいですか? (できればapply_asyncを使うのが望ましいですが、結果の順序は維持する必要があります。)

私の知る限り、この動作はapply_asyncだけでなく、他のpoolメソッドにも固有です。 なぜこれが起こるのか、そして方法の振る舞いが私が達成したい行動に変わりました。バウンティは、両方のクエリに回答を提供できる回答に行きます。

4
Bram Vanroy

次のことが起こっていると思います:

  1. 毎回 self.processが呼び出され、メソッドがシリアル化(ピクル化)され、子プロセスに送信されます。毎回新しいコピーが作成されます。
  2. このメソッドは子プロセスで実行されますが、親プロセスの元のプロセスとは別のコピーの一部であるため、変更された状態は親プロセスに影響せず、影響を与えることもありません。返される唯一の情報は、戻り値(同様に漬けられている)です。

子プロセスにはMultitestの独自のインスタンスがないことに注意してください。これは、__name__ == '__main__'プールによって作成されたフォークには適用されません。

子プロセスの状態を維持したい場合は、グローバル変数を使用して行うことができます。そのような変数を初期化するプールを作成するときに、初期化引数を渡すことができます。

以下は、意図したものの作業バージョンを示しています(ただし、OOPがないため、マルチプロセッシングではうまく機能しません)。

from multiprocessing.pool import Pool


def initialize():
    global I
    I = 0


def process(inp):
    global I
    print("I", I)
    I += 1
    return inp


if __name__ == '__main__':
    with Pool(2, initializer=initialize) as pool:
        worker_jobs = []
        for j in range(10):
            job = pool.apply_async(process, (j,))
            worker_jobs.append(job)

        for job in worker_jobs:
            res = job.get()
            print("input", res)
1
Andreas

マルチプロセッシングとスレッド化の1つの違いは、プロセスが作成された後、そのプロセスが使用するメモリは親プロセスから仮想的に複製されるため、プロセス間で共有メモリが存在しないことです。

次に例を示します。

import os
import time
from threading import Thread

global_counter = 0

def my_thread():
    global global_counter
    print("in thread, global_counter is %r, add one." % global_counter)
    global_counter += 1

def test_thread():
    global global_counter
    th = Thread(target=my_thread)
    th.start()
    th.join()
    print("in parent, child thread joined, global_counter is %r now." % global_counter)

def test_fork():
    global global_counter
    pid = os.fork()
    if pid == 0:
        print("in child process, global_counter is %r, add one." % global_counter)
        global_counter += 1
        exit()
    time.sleep(1)
    print("in parent, child process died, global_counter is still %r." % global_counter)

def main():
    test_thread()
    test_fork()

if __name__ == "__main__":
    main()

出力:

in thread, global_counter is 0, add one.
in parent, child thread joined, global_counter is 1 now.
in child process, global_counter is 1, add one.
in parent, child process died, global_counter is still 1.

あなたの場合:

for j in range(10):
    # Before fork, self.i is 0, fork() dups memory, so the variable is not shared to the child.
    job = pool.apply_async(self.process, (j,))
    # After job finishes, child's self.i is 1 (not parent's), this variable is freed after child dies.
    worker_jobs.append(job)

編集:

Python3では、バインドされたメソッドのpickle化にはオブジェクト自体も含まれ、基本的にそれを複製します。したがって、毎回apply_asyncが呼び出され、オブジェクトselfもピクルされます。

import os
from multiprocessing.pool import Pool
import pickle

class Multitest:
    def __init__(self):
        self.i = "myattr"

    def run(self):
        with Pool(2) as pool:
            worker_jobs = []
            for j in range(10):
                job = pool.apply_async(self.process, (j,))
                worker_jobs.append(job)

            for job in worker_jobs:
                res = job.get()
                print("input", res)

    def process(self, inp):
        print("i", self.i)
        self.i += "|append"

        return inp

def test_pickle():
    m = Multitest()
    print("original instance is %r" % m)

    pickled_method = pickle.dumps(m.process)
    assert b"myattr" in pickled_method

    unpickled_method = pickle.loads(pickled_method)
    # get instance from it's method (python 3)
    print("pickle duplicates the instance, new instance is %r" % unpickled_method.__self__)

if __name__ == '__main__':
    test_pickle()

出力:

original instance is <__main__.Multitest object at 0x1072828d0>
pickle duplicates the instance, new instance is <__main__.Multitest object at 0x107283110>
0
Kamoo