web-dev-qa-db-ja.com

Pythonでループ内の操作をマルチスレッドする方法

非常に大きなリストがあり、次のような操作を実行しているとします。

for item in items:
    try:
        api.my_operation(item)
    except:
        print 'error with item'

私の問題は2つあります。

  • たくさんのアイテムがあります
  • api.my_operationが戻るまでに時間がかかります

マルチスレッドを使用してapi.my_operationsの束を一度にスピンアップして、5個または10個、さらには100個のアイテムを一度に処理できるようにします。

My_operation()が例外を返した場合(おそらくそのアイテムを既に処理しているため)-それで構いません。何も壊しません。ループは次の項目に進むことができます。

:これはPython 2.7.3

45
doremi

まず、PythonでコードがCPUにバインドされている場合、グローバルインタープリターロックを保持できるスレッドは1つだけなので、マルチスレッドは役に立ちません。したがって、一度にPythonコードを実行します。 、スレッドではなくプロセスを使用する必要があります。

これは、オペレーションがIOにバインドされているため(つまり、ネットワークやディスクのコピーなどを待機しているため)、「戻るまで永遠にかかる」場合には当てはまりません。後でまた説明します。


次に、5個、10個、または100個のアイテムを一度に処理する方法は、5個、10個、または100個のワーカーのプールを作成し、ワーカーがサービスを提供するキューにアイテムを入れることです。幸いなことに、stdlib multiprocessing および concurrent.futures ライブラリはどちらも詳細の大部分をまとめています。

前者は、従来のプログラミングに対してより強力で柔軟です。将来の待機を作成する必要がある場合は、後者の方が簡単です。些細なケースでは、どちらを選択してもかまいません。 (この場合、それぞれの最も明白な実装は、futuresで3行、multiprocessingで4行かかります。)

2.6-2.7または3.0-3.1を使用している場合、futuresは組み込まれていませんが、 PyPIpip install futures)。


最後に、ループの繰り返し全体を関数呼び出し(たとえば、mapに渡すことができるもの)に変えることができる場合、通常は物事を並列化する方がはるかに簡単なので、最初にそれをしましょう:

def try_my_operation(item):
    try:
        api.my_operation(item)
    except:
        print('error with item')

すべてを一緒に入れて:

executor = concurrent.futures.ProcessPoolExecutor(10)
futures = [executor.submit(try_my_operation, item) for item in items]
concurrent.futures.wait(futures)

比較的小さなジョブが多数ある場合、マルチプロセッシングのオーバーヘッドがゲインを圧倒する可能性があります。それを解決する方法は、作業をより大きなジョブにまとめ上げることです。たとえば、 grouperレシピのitertoolsを使用 をコピーしてコードに貼り付けたり、more-itertools PyPIのプロジェクト):

def try_multiple_operations(items):
    for item in items:
        try:
            api.my_operation(item)
        except:
            print('error with item')

executor = concurrent.futures.ProcessPoolExecutor(10)
futures = [executor.submit(try_multiple_operations, group) 
           for group in grouper(5, items)]
concurrent.futures.wait(futures)

最後に、コードがIO bound?の場合、スレッドはプロセスと同じくらい優れており、オーバーヘッドが少ない(および制限が少ないが、これらの制限は通常このような場合には影響しません) )。時々、「オーバーヘッドが少ない」ということは、スレッドでバッチ処理する必要はないが、プロセスで処理することを意味するのに十分な場合があり、これは素晴らしい勝利です。

それでは、プロセスの代わりにスレッドをどのように使用しますか? ProcessPoolExecutorThreadPoolExecutorに変更するだけです。

コードがCPUバウンドかIOバウンドかわからない場合は、両方の方法で試してください。


pythonスクリプト内の複数の関数に対してこれを行うことはできますか?たとえば、並列化したいコードの他の場所に別のforループがあった場合、2つのマルチスレッド関数を実行できますか?同じスクリプト?

はい。実際、それを行うには2つの異なる方法があります。

まず、同じ(スレッドまたはプロセス)エグゼキューターを共有し、複数の場所から問題なく使用できます。タスクと将来の全体的なポイントは、それらが自己完結型であることです。それらがどこで実行されているかは気にせず、それらをキューに入れて、最終的に答えを得るだけです。

または、同じプログラムに2つのエグゼキューターを問題なく含めることができます。これにはパフォーマンスコストがかかります。両方のエグゼキュータを同時に使用している場合、8コアで16個のビジースレッドを実行しようとすることになります。つまり、コンテキストの切り替えが発生します。ただし、2つのエグゼキュータが同時にビジーになることはめったにないため、実行する価値がある場合もあります。これにより、コードが非常に簡単になります。または、1つのエグゼキューターが完了に時間がかかる非常に大きなタスクを実行しており、もう1つのエグゼキューターがプログラムの一部のスループットよりも応答性が重要であるため、できるだけ早く完了する必要がある非常に小さなタスクを実行している場合があります。

どちらがあなたのプログラムに適しているかわからない場合、通常はそれが最初です。

83
abarnert

2018-02-06の編集このコメント に基づく改訂

Edit:これがPython 2.7.x

Multiprocesing.poolがあり、次のサンプルはそのうちの1つを使用する方法を示しています。

from multiprocessing.pool import ThreadPool as Pool
# from multiprocessing import Pool

pool_size = 5  # your "parallelness"

# define worker function before a Pool is instantiated
def worker(item):
    try:
        api.my_operation(item)
    except:
        print('error with item')

pool = Pool(pool_size)

for item in items:
    pool.apply_async(worker, (item,))

pool.close()
pool.join()

@abarnertが述べたように、プロセスがCPUにバインドされていることを実際に特定した場合、ThreadPoolをプロセスプール実装(ThreadPoolインポートの下でコメント)に変更します。詳細についてはこちらをご覧ください: http://docs.python.org/2/library/multiprocessing.html#using-a-pool-of-workers

21
woozyking

次のようなアプローチを使用して、処理を指定された数のスレッドに分割できます。

import threading                                                                

def process(items, start, end):                                                 
    for item in items[start:end]:                                               
        try:                                                                    
            api.my_operation(item)                                              
        except Exception:                                                       
            print('error with item')                                            


def split_processing(items, num_splits=4):                                      
    split_size = len(items) // num_splits                                       
    threads = []                                                                
    for i in range(num_splits):                                                 
        # determine the indices of the list this thread will handle             
        start = i * split_size                                                  
        # special case on the last chunk to account for uneven splits           
        end = None if i+1 == num_splits else (i+1) * split_size                 
        # create the thread                                                     
        threads.append(                                                         
            threading.Thread(target=process, args=(items, start, end)))         
        threads[-1].start() # start the thread we just created                  

    # wait for all threads to finish                                            
    for t in threads:                                                           
        t.join()                                                                



split_processing(items)
10
Ryan Haining
import numpy as np
import threading


def threaded_process(items_chunk):
    """ Your main process which runs in thread for each chunk"""
    for item in items_chunk:                                               
        try:                                                                    
            api.my_operation(item)                                              
        except Exception:                                                       
            print('error with item')  

n_threads = 20
# Splitting the items into chunks equal to number of threads
array_chunk = np.array_split(input_image_list, n_threads)
thread_list = []
for thr in range(n_threads):
    thread = threading.Thread(target=threaded_process, args=(array_chunk[thr]),)
    thread_list.append(thread)
    thread_list[thr].start()

for thread in thread_list:
    thread.join()
0