web-dev-qa-db-ja.com

ThreadPoolExecutor()。mapはThreadPoolExecutor()。submitとどう違うのですか?

私が書いたいくつかのコードにとても戸惑っていました。私はそれを発見して驚きました:

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))

異なる結果を生み出します。 1つ目はfが返すタイプのリストを作成し、2つ目はconcurrent.futures.Futureオブジェクトのリストを作成します。これらのオブジェクトは、値を取得するためにresult()メソッドで評価する必要がありますfが返しました。

私の主な関心事は、これはexecutor.mapconcurrent.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を使用します。そうするのは間違っていますか?

35
Patrick Collins

問題は、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_stuffcrunch_numberよりも時間がかかる可能性があり、これが実際に当てはまる場合、mapの簡単な使用を維持しながらパフォーマンスの大きな損失ではありません。

また、ワーカースレッド(/プロセス)はリストの先頭から処理を開始し、送信したリストの最後まで処理するため、結果はイテレータによって既に生成された順序で完了する必要があります。つまり、ほとんどの場合executor.mapは問題ありませんが、たとえば、値を処理する順序とmapに渡した関数の処理順序が重要でない場合など、時間がかかる場合があります実行するには、future.as_completedの方が高速です。

23
Kritzefitz

以下は、送信とマップの例です。両方ともすぐにジョブを受け入れます(submitted | mapped-start)。それらは完了するのに同じ時間、11秒かかります(最後の結果時間-開始)。ただし、submitは、ThreadPoolExecutor maxThreads = 2のスレッドが完了するとすぐに結果を出します。マップは、送信された順序で結果を提供します。

import time
import concurrent.futures

def worker(i):
    time.sleep(i)
    return i,time.time()

e = concurrent.futures.ThreadPoolExecutor(2)
arrIn = range(1,7)[::-1]
print arrIn

f = []
print 'start submit',time.time()
for i in arrIn:
    f.append(e.submit(worker,i))
print 'submitted',time.time()
for r in concurrent.futures.as_completed(f):
    print r.result(),time.time()
print

f = []
print 'start map',time.time()
f = e.map(worker,arrIn)
print 'mapped',time.time()
for r in f:
    print r,time.time()    

出力:

[6, 5, 4, 3, 2, 1]
start submit 1543473934.47
submitted 1543473934.47
(5, 1543473939.473743) 1543473939.47
(6, 1543473940.471591) 1543473940.47
(3, 1543473943.473639) 1543473943.47
(4, 1543473943.474192) 1543473943.47
(1, 1543473944.474617) 1543473944.47
(2, 1543473945.477609) 1543473945.48

start map 1543473945.48
mapped 1543473945.48
(6, 1543473951.483908) 1543473951.48
(5, 1543473950.484109) 1543473951.48
(4, 1543473954.48858) 1543473954.49
(3, 1543473954.488384) 1543473954.49
(2, 1543473956.493789) 1543473956.49
(1, 1543473955.493888) 1543473956.49
7
edW

ここの回答の説明に加えて、ソースに直接アクセスすると役立つ場合があります。ここでの別の答えからの声明を再確認します:

  • .map()は、送信された順序で結果を提供しますが、
  • concurrent.futures.as_completed() を使用してFutureオブジェクトのリストを繰り返し処理しても、この順序は保証されません。 as_completed()

.map()は基本クラスで定義されています _concurrent.futures._base.Executor_

_class Executor(object):
    def submit(self, fn, *args, **kwargs):
        raise NotImplementedError()

    def map(self, fn, *iterables, timeout=None, chunksize=1):
        if timeout is not None:
            end_time = timeout + time.monotonic()

        fs = [self.submit(fn, *args) for args in Zip(*iterables)]  # <!!!!!!!!

        def result_iterator():
            try:
                # reverse to keep finishing order
                fs.reverse()  # <!!!!!!!!
                while fs:
                    # Careful not to keep a reference to the popped future
                    if timeout is None:
                        yield fs.pop().result()  # <!!!!!!!!
                    else:
                        yield fs.pop().result(end_time - time.monotonic())
            finally:
                for future in fs:
                    future.cancel()
        return result_iterator()
_

言及したように、.submit()もあります。これは、子クラス、つまりProcessPoolExecutorThreadPoolExecutor 、そして実際に何かをするために.result()を呼び出す必要がある__base.Future_インスタンスを返します。

.map()の重要な行は次のように要約されます。

_fs = [self.submit(fn, *args) for args in Zip(*iterables)]
fs.reverse()
while fs:
    yield fs.pop().result()
_

.reverse().pop()は、最初に送信された結果(iterablesから)を最初に取得し、2番目に送信された結果を2番目に取得する手段です。オン。結果のイテレータの要素はFuturesではありません。それらは実際の結果そのものです。

6
Brad Solomon