1. ホーム
  2. python

[解決済み] ThreadPoolExecutor().mapとThreadPoolExecutor().submitはどう違うのですか?

2023-03-19 09:54:14

質問

私は自分が書いたあるコードにとても困惑していました。私はそれを発見して驚きました。

with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(f, iterable))

with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
    results = list(map(lambda x: executor.submit(f, x), iterable))

は異なる結果を生成します。最初のものは、どんな型のリストであっても f が返すリストを生成し、2番目は concurrent.futures.Future オブジェクトのリストを生成し、それを result() メソッドで評価する必要があります。 f が返した値を取得するためです。

私の主な懸念は、これがつまり executor.map を利用することができないということです。 concurrent.futures.as_completed を利用することができません。これは、データベースへの長期にわたる呼び出しの結果が利用可能になったときに、その結果を評価するための非常に便利な方法のように思えます。

私は、どのように concurrent.futures.ThreadPoolExecutor オブジェクトがどのように機能するのか、全く理解していません。素朴に、私は (多少冗長な) 方がいいと思います。

with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
    result_futures = list(map(lambda x: executor.submit(f, x), iterable))
    results = [f.result() for f in futures.as_completed(result_futures)]

より簡潔な executor.map を使うことで、パフォーマンスが向上する可能性があります。私はそうすることが間違っているのでしょうか?

どのように解決するのですか?

の結果を変換するのが問題です。 ThreadPoolExecutor.map の結果をリストに変換していることです。これを行わず、結果のジェネレータを直接反復処理すると、結果は元の順序のまま得られますが、すべての結果が準備できる前にループが続行されます。この例でテストすることができます。

import time
import concurrent.futures

e = concurrent.futures.ThreadPoolExecutor(4)
s = range(10)
for i in e.map(time.sleep, s):
    print(i)

順番が維持される理由は、マップに与えた順番と同じ順番で結果を得ることが重要な場合があるからでしょう。そして、結果はおそらく未来オブジェクトにラップされません。なぜなら、状況によっては、すべての結果を得るためにリストに対して別のマップを実行するには時間がかかりすぎるからです。そして、ほとんどの場合、ループが最初の値を処理する前に次の値が用意されている可能性が高いのです。これはこの例で実証されています。

import concurrent.futures

executor = concurrent.futures.ThreadPoolExecutor() # Or ProcessPoolExecutor
data = some_huge_list()
results = executor.map(crunch_number, data)
finals = []

for value in results:
    finals.append(do_some_stuff(value))

この例では、おそらく do_some_stuff よりも時間がかかるかもしれません。 crunch_number であり、これが本当にそうであるならば、マップの簡単な使い方を維持したまま、パフォーマンスの大きな損失にはなりません。

また、ワーカスレッド(/processes)はリストの先頭から処理を開始し、提出されたリストの末尾に向かうので、結果はすでにイテレータによってもたらされた順番に終了するはずです。つまり、ほとんどの場合 executor.map はちょうどいいのですが、 場合によっては、例えばどの順番で値を処理するかは重要でなく map に渡した関数が実行されるまでにかかる時間が大きく異なる場合などには future.as_completed の方が速いかもしれません。