web-dev-qa-db-ja.com

PythonマルチプロセッシングでPool.mapを配列(共有メモリ)と組み合わせる方法は?

複数のプロセスで並行して処理したい非常に大きな(読み取り専用)データの配列があります。

Pool.map関数が好きで、それを使用してそのデータの関数を並列で計算したいと思います。

ValueクラスまたはArrayクラスを使用して、プロセス間で共有メモリデータを使用できることがわかりました。しかし、これを使用しようとすると、RuntimeErrorが発生します。「SynchronizedStringオブジェクトは、Pool.map関数を使用する場合、継承を通じてプロセス間でのみ共有する必要があります。

これが私がやろうとしていることの簡単な例です:

from sys import stdin
from multiprocessing import Pool, Array

def count_it( arr, key ):
  count = 0
  for c in arr:
    if c == key:
      count += 1
  return count

if __== '__main__':
  testData = "abcabcs bsdfsdf gdfg dffdgdfg sdfsdfsd sdfdsfsdf"
  # want to share it using shared memory
  toShare = Array('c', testData)

  # this works
  print count_it( toShare, "a" )

  pool = Pool()

  # RuntimeError here
  print pool.map( count_it, [(toShare,key) for key in ["a", "b", "s", "d"]] )

誰が私がここで間違っているのか教えてもらえますか?

だから私がやりたいのは、プロセスがプロセスプールで作成された後、新しく作成された共有メモリ割り当て配列に関する情報をプロセスに渡すことです。

51
Jeroen Dirks

私はちょうど賞金を見たようにもう一度試してください;)

基本的に、エラーメッセージはそれが言ったことを意味すると思います-マルチプロセス共有メモリ配列を引数として渡すことはできません(酸洗いによって)。データをシリアル化することは意味がありません。ポイントはデータが共有メモリであることです。そのため、共有配列をグローバルにする必要があります。私の最初の答えのように、モジュールの属性としてそれを置くのはいいと思いますが、あなたの例ではグローバル変数としてそれを残すだけでもうまくいきます。フォークの前にデータを設定したくないというあなたの意見を取り入れて、ここに修正された例があります。複数の可能な共有配列が必要な場合(そして引数としてtoShareを渡したい理由です)、同様に共有配列のグローバルリストを作成し、count_itにインデックスを渡すことができます(for c in toShare[i]:になります)。

from sys import stdin
from multiprocessing import Pool, Array, Process

def count_it( key ):
  count = 0
  for c in toShare:
    if c == key:
      count += 1
  return count

if __== '__main__':
  # allocate shared array - want lock=False in this case since we 
  # aren't writing to it and want to allow multiple processes to access
  # at the same time - I think with lock=True there would be little or 
  # no speedup
  maxLength = 50
  toShare = Array('c', maxLength, lock=False)

  # fork
  pool = Pool()

  # can set data after fork
  testData = "abcabcs bsdfsdf gdfg dffdgdfg sdfsdfsd sdfdsfsdf"
  if len(testData) > maxLength:
      raise ValueError, "Shared array too small to hold data"
  toShare[:len(testData)] = testData

  print pool.map( count_it, ["a", "b", "s", "d"] )

[編集:forkを使用していないため、上記はWindowsでは機能しません。ただし、以下はWindowsで動作し、まだプールを使用しているので、これはあなたが望むものに最も近いと思います:

from sys import stdin
from multiprocessing import Pool, Array, Process
import mymodule

def count_it( key ):
  count = 0
  for c in mymodule.toShare:
    if c == key:
      count += 1
  return count

def initProcess(share):
  mymodule.toShare = share

if __== '__main__':
  # allocate shared array - want lock=False in this case since we 
  # aren't writing to it and want to allow multiple processes to access
  # at the same time - I think with lock=True there would be little or 
  # no speedup
  maxLength = 50
  toShare = Array('c', maxLength, lock=False)

  # fork
  pool = Pool(initializer=initProcess,initargs=(toShare,))

  # can set data after fork
  testData = "abcabcs bsdfsdf gdfg dffdgdfg sdfsdfsd sdfdsfsdf"
  if len(testData) > maxLength:
      raise ValueError, "Shared array too small to hold data"
  toShare[:len(testData)] = testData

  print pool.map( count_it, ["a", "b", "s", "d"] )

なぜマップが配列をピクルしないのかわかりませんが、プロセスとプールはそうします-ウィンドウ上のサブプロセスの初期化の時点でおそらく転送されたと思います。ただし、データはforkの後も設定されます。

41
robince

私が見る問題は、プールが引数リストを介した共有データのピクルをサポートしていないことです。これは、「継承によってオブジェクトをプロセス間でのみ共有する必要がある」というエラーメッセージの意味です。共有データは継承する必要があります。つまり、Poolクラスを使用して共有データを共有する場合は、グローバルにする必要があります。

明示的に渡す必要がある場合は、multiprocessing.Processを使用する必要があります。修正した例を次に示します。

from multiprocessing import Process, Array, Queue

def count_it( q, arr, key ):
  count = 0
  for c in arr:
    if c == key:
      count += 1
  q.put((key, count))

if __== '__main__':
  testData = "abcabcs bsdfsdf gdfg dffdgdfg sdfsdfsd sdfdsfsdf"
  # want to share it using shared memory
  toShare = Array('c', testData)

  q = Queue()
  keys = ['a', 'b', 's', 'd']
  workers = [Process(target=count_it, args = (q, toShare, key))
    for key in keys]

  for p in workers:
    p.start()
  for p in workers:
    p.join()
  while not q.empty():
    print q.get(),

出力:( 's'、9)( 'a'、2)( 'b'、3)( 'd'、12)

キューの要素の順序は異なる場合があります。

これをより一般的でプールに似たものにするために、N個の固定プロセスを作成し、キーのリストをN個に分割し、リスト内の各キーに対してcount_itを呼び出すラッパー関数をProcessターゲットとして使用できます次のように渡されます:

def wrapper( q, arr, keys ):
  for k in keys:
    count_it(q, arr, k)
5
jwilson

データが読み取り専用の場合は、モジュール内の変数にしてくださいbefore Poolからの分岐。そうすれば、すべての子プロセスがそれにアクセスできるはずです。また、書き込みをしない限りコピーされません。

import myglobals # anything (empty .py file)
myglobals.data = []

def count_it( key ):
    count = 0
    for c in myglobals.data:
        if c == key:
            count += 1
    return count

if __== '__main__':
myglobals.data = "abcabcs bsdfsdf gdfg dffdgdfg sdfsdfsd sdfdsfsdf"

pool = Pool()
print pool.map( count_it, ["a", "b", "s", "d"] )

lock=Falseキーワード引数(デフォルトではtrue)を試してみることもできますが、配列を使用したい場合。

2
robince