Как многопоточность операции в цикле в Python

скажем, у меня очень большой список, и я выполняю такую операцию:

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

моя проблема в два раза:

  • есть много пунктов
  • API-интерфейс.my_operation принимает навсегда, чтобы вернуться

Я хотел бы использовать многопоточность, чтобы раскрутить кучу api.my_operations сразу, чтобы я мог обрабатывать, возможно, 5 или 10 или даже 100 элементов сразу.

Если my_operation () возвращает исключение (потому что, возможно, я уже обработал этот пункт) - это нормально. Он ничего не сломает. Цикл может продолжаться до следующего элемента.

Примечание: это для Python 2.7.3

3 ответов


во-первых, в Python, если ваш код привязан к процессору, многопоточность не поможет, потому что только один поток может содержать глобальную блокировку интерпретатора и, следовательно, запускать код Python за раз. Итак, вам нужно использовать процессы, а не потоки.

это неверно, если ваша операция "берет навсегда, чтобы вернуться", потому что она связана с IO-то есть ждет в сети или дисковых копиях или тому подобное. Я вернусь к этому позже.


далее, путь обрабатывать 5 или 10 или 100 деталей сразу же нужно создать пул из 5 или 10 или 100 работников и поместить элементы в очередь, которую обслуживают работники. К счастью, stdlib multiprocessing и concurrent.futures библиотеки оба завершает большую часть деталей для вас.

первый более мощный и гибкий для традиционного программирования; последний проще, если вам нужно составить будущее ожидание; для тривиальных случаев действительно не имеет значения, какой вы выбираете. (В этом случае наиболее очевидным реализация с каждым занимает 3 строки с futures, 4 строки с multiprocessing.)

если вы используете 2.6-2.7 или 3.0-3.1, futures не встроен, но вы можете установить его из PyPI (pip 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? Затем потоки как процессы, и с меньшими накладными расходами (и меньшими ограничениями, но эти ограничения обычно не влияют на вас в таких случаях). Иногда этого "меньше накладных расходов" достаточно, чтобы означать, что вам не нужно паковать с потоками, но вы делаете с процессами, что является хорошей победой.

Итак, как вы используете потоки вместо процессов? Просто изменить ProcessPoolExecutor to ThreadPoolExecutor.

если вы не уверены, привязан ли ваш код к CPU или IO, просто попробуйте оба способа.


могу ли я сделать это для нескольких функций в моем скрипте python? Например, если у меня был другой цикл for в другом месте кода, который я хотел распараллелить. Можно ли выполнять две многопоточные функции в одном скрипте?

да. На самом деле, есть два разных способа сделать это.

во-первых, вы можете использовать один и тот же (поток или процесс) исполнитель и использовать его из нескольких мест без проблем. Весь смысл задач и будущего в том, что они автономный; вам все равно, куда они бегут, просто вы выстраиваете их в очередь и в конечном итоге получаете ответ.

кроме того, вы можете иметь двух исполнителей в одной программе без проблем. Это имеет стоимость производительности-если вы используете обоих исполнителей одновременно, вы в конечном итоге попытаетесь запустить (например) 16 занятых потоков на 8 ядрах, что означает, что будет некоторое переключение контекста. Но иногда это стоит сделать, потому что, скажем, два исполнителя редко заняты в же время, и это делает ваш код намного проще. Или, может быть, один исполнитель выполняет очень большие задачи, которые могут занять некоторое время, а другой выполняет очень маленькие задачи, которые необходимо выполнить как можно быстрее, потому что отзывчивость более важна, чем пропускная способность для части вашей программы.

если вы не знаете, что подходит для вашей программы, как правило, это первое.


редактировать 2018-02-06: пересмотр на основе комментарий

редактировать: забыл упомянуть, что это работает на Python 2.7.x

там multiprocesing.пул, и следующий пример иллюстрирует, как использовать один из них:

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, измените ThreadPool на реализацию пула процессов (прокомментировано в разделе импорт ThreadPool). Вы можете найти более подробная информация здесь: http://docs.python.org/2/library/multiprocessing.html#using-a-pool-of-workers


вы можете разделить обработку на определенное количество потоков, используя такой подход:

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)