Як паралелізувати просту петлю Python?


255

Це, мабуть, тривіальне питання, але як я паралелізую наступний цикл у python?

# setup output lists
output1 = list()
output2 = list()
output3 = list()

for j in range(0, 10):
    # calc individual parameter value
    parameter = j * offset
    # call the calculation
    out1, out2, out3 = calc_stuff(parameter = parameter)

    # put results into correct output list
    output1.append(out1)
    output2.append(out2)
    output3.append(out3)

Я знаю, як запустити окремі потоки в Python, але я не знаю, як "зібрати" результати.

Кілька процесів теж було б добре - все, що найпростіше для цього випадку. Зараз я використовую Linux, але код також повинен працювати в Windows та Mac.

Який найпростіший спосіб паралелізувати цей код?

Відповіді:


191

Використання декількох потоків на CPython не дасть кращої продуктивності для чистого Python-коду завдяки глобальному блокуванню інтерпретатора (GIL). Я пропоную multiprocessingзамість цього використовувати модуль:

pool = multiprocessing.Pool(4)
out1, out2, out3 = zip(*pool.map(calc_stuff, range(0, 10 * offset, offset)))

Зауважте, що це не працюватиме в інтерактивному перекладачі.

Щоб уникнути звичайного FUD навколо GIL: Використовувати потоки для цього прикладу не було б жодної переваги. Ви хочете використовувати тут процеси, а не потоки, тому що вони уникають цілого ряду проблем.


46
Оскільки це обрана відповідь, чи можна мати більш всебічний приклад? Про які аргументи calc_stuff?
Едуардо Піньятеллі

2
@EduardoPignatelli Будь ласка, прочитайте документацію multiprocessingмодуля, щоб отримати більш вичерпні приклади. Pool.map()в основному працює як map(), але паралельно.
Свен Марнах

3
Чи є спосіб просто додати в панель завантаження tqdm до цієї структури коду? Я використовував tqdm (pool.imap (calc_stuff, діапазон (0, 10 * зміщення, зміщення))), але я не отримую повну графічну смужку завантаження.
user8188120

@ user8188120 Я ніколи не чув про tqdm, тому вибачте, я не можу з цим допомогти.
Свен Марнах

Для панелі завантаження tqdm дивіться це запитання: stackoverflow.com/questions/41920124/…
Йоханнес

66

Щоб паралелізувати просту для циклу, joblib приносить велику цінність сировинному використанню багатопроцесорної обробки. Не тільки короткий синтаксис, але й такі речі, як прозора купка ітерацій, коли вони дуже швидкі (щоб зняти накладні витрати) або фіксують простеження дочірнього процесу, щоб мати кращі повідомлення про помилки.

Відмова: Я є оригінальним автором joblib.


1
Я спробував джобліб з юпітером, це не працює. Після паралельно затримки дзвінка сторінка припинила свою роботу.
Jie

1
Привіт, у мене є проблема з використанням joblib ( stackoverflow.com/questions/52166572 / ... ), у вас є якісь - або підказки , що може бути причиною? Дуже дякую.
Ting Sun

Здається, щось, що я хочу дати зняти! Чи можна використовувати його з подвійним циклом, наприклад, для i в діапазоні (10): для j в діапазоні (20)
CutePoison

51

Який найпростіший спосіб паралелізувати цей код?

Мені це дуже подобається concurrent.futures, доступний у Python3 з версії 3.2 - і через backport до 2.6 та 2.7 на PyPi .

Ви можете використовувати потоки чи процеси та використовувати той самий інтерфейс.

Багатопроцесорна

Помістіть це у файл - futuretest.py:

import concurrent.futures
import time, random               # add some random sleep time

offset = 2                        # you don't supply these so
def calc_stuff(parameter=None):   # these are examples.
    sleep_time = random.choice([0, 1, 2, 3, 4, 5])
    time.sleep(sleep_time)
    return parameter / 2, sleep_time, parameter * parameter

def procedure(j):                 # just factoring out the
    parameter = j * offset        # procedure
    # call the calculation
    return calc_stuff(parameter=parameter)

def main():
    output1 = list()
    output2 = list()
    output3 = list()
    start = time.time()           # let's see how long this takes

    # we can swap out ProcessPoolExecutor for ThreadPoolExecutor
    with concurrent.futures.ProcessPoolExecutor() as executor:
        for out1, out2, out3 in executor.map(procedure, range(0, 10)):
            # put results into correct output list
            output1.append(out1)
            output2.append(out2)
            output3.append(out3)
    finish = time.time()
    # these kinds of format strings are only available on Python 3.6:
    # time to upgrade!
    print(f'original inputs: {repr(output1)}')
    print(f'total time to execute {sum(output2)} = sum({repr(output2)})')
    print(f'time saved by parallelizing: {sum(output2) - (finish-start)}')
    print(f'returned in order given: {repr(output3)}')

if __name__ == '__main__':
    main()

І ось результат:

$ python3 -m futuretest
original inputs: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
total time to execute 33 = sum([0, 3, 3, 4, 3, 5, 1, 5, 5, 4])
time saved by parallellizing: 27.68999981880188
returned in order given: [0, 4, 16, 36, 64, 100, 144, 196, 256, 324]

Багатопотоковість

Тепер перейдіть ProcessPoolExecutorна ThreadPoolExecutorмодуль та запустіть його ще раз:

$ python3 -m futuretest
original inputs: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
total time to execute 19 = sum([0, 2, 3, 5, 2, 0, 0, 3, 3, 1])
time saved by parallellizing: 13.992000102996826
returned in order given: [0, 4, 16, 36, 64, 100, 144, 196, 256, 324]

Тепер ви зробили як багатопотокове, так і багатопроцесорне!

Примітка щодо продуктивності та використання обох разом.

Відбір проб є занадто малим для порівняння результатів.

Однак я підозрюю, що багатопотокове читання буде швидше, ніж багатопроцесове взагалі, особливо в Windows, оскільки Windows не підтримує розгортання, тому кожен новий процес повинен зайняти час для запуску. На Linux або Mac вони, мабуть, будуть ближче.

Ви можете вкласти декілька потоків у декілька процесів, але рекомендується не використовувати декілька потоків для відкручування декількох процесів.


чи ThreadPoolExecutor обходить обмеження, накладені GIL? також не хочете, щоб ви приєдналися (), щоб дочекатися, коли виконавці закінчать, або це негайно потурбується всередині менеджера контексту
PirateApp

1
Ні і ні, так, щоб "обробляти неявно"
Аарон Холл

Чомусь під час масштабування проблеми багатопотоковість надзвичайно швидка, але багатопроцесорна породжує купу застряглих процесів (у macOS). Будь-яка ідея, чому це могло бути? Процес містить лише вкладені петлі та математику, нічого екзотичного.
komodovaran_

@komodovaran_ Процес - це повний процес Python, по одному на кожного, тоді як потік - це лише нитка виконання зі своїм стеком, який ділиться процесом, його байт-кодом та всім іншим, що він має в пам'яті, з усіма іншими потоками - чи допомагає це ?
Аарон Холл

49
from joblib import Parallel, delayed
import multiprocessing

inputs = range(10) 
def processInput(i):
    return i * i

num_cores = multiprocessing.cpu_count()

results = Parallel(n_jobs=num_cores)(delayed(processInput)(i) for i in inputs)
print(results)

Вищезазначене прекрасно працює на моїй машині (Ubuntu, пакет joblib був попередньо встановлений, але його можна встановити через pip install joblib).

Взяте з https://blog.dominodatalab.com/simple-parallelization/


3
Я спробував ваш код, але в моїй системі послідовна версія цього коду займає близько півхвилини, а вищевказана паралельна версія займає 4 хвилини. Чому так?
шайфалі Гупта

3
Дякую за вашу відповідь! Я думаю, що це найелегантніший спосіб зробити це у 2019 році.
Хейкі

2
багатопроцесорна робота недійсна для Python 3.x, тому для мене це не працює.
EngrStudent

2
@EngrStudent Не впевнений, що ви маєте на увазі під "недійсним". Він працює для Python 3.6.x для мене.
тирекс

@tyrex дякую за обмін! цей пакунок joblib чудовий, і приклад працює для мене. Хоча, у складнішому контексті, на жаль, у мене була помилка. github.com/joblib/joblib/issues/949
Відкритий брокер з

13

Є ряд переваг використання Ray :

  • Ви можете проводити паралель на декількох машинах на додаток до декількох ядер (з тим самим кодом).
  • Ефективна обробка числових даних за допомогою спільної пам'яті (і серіалізація з нульовою копією).
  • Висока пропускна здатність із розподіленим плануванням.
  • Відмовостійкість.

У вашому випадку ви можете запустити Ray і визначити віддалену функцію

import ray

ray.init()

@ray.remote(num_return_vals=3)
def calc_stuff(parameter=None):
    # Do something.
    return 1, 2, 3

а потім викликати його паралельно

output1, output2, output3 = [], [], []

# Launch the tasks.
for j in range(10):
    id1, id2, id3 = calc_stuff.remote(parameter=j)
    output1.append(id1)
    output2.append(id2)
    output3.append(id3)

# Block until the results have finished and get the results.
output1 = ray.get(output1)
output2 = ray.get(output2)
output3 = ray.get(output3)

Щоб запустити той самий приклад на кластері, єдиний рядок, який змінився би, був викликом ray.init (). Відповідну документацію можна знайти тут .

Зауважте, що я допомагаю розвивати Рея.


1
Для всіх, хто розглядає ray, може бути доречним знати, що він не підтримує Windows. Можливі деякі хаки, щоб змусити його працювати в Windows за допомогою WSL (підсистема Windows для Linux), хоча навряд чи це нестандартно, якщо ви хочете використовувати Windows.
OscarVanL

9

Це найпростіший спосіб зробити це!

Можна використовувати асинціо . (Документацію можна знайти тут ). Він використовується як основа для безлічі асинхронних фреймворків Python, які забезпечують високопродуктивні мережеві та веб-сервери, бібліотеки підключення до бази даних, розподілені черги завдань тощо. Плюс до того ж, він має як API високого, так і низького рівня для вирішення будь-яких проблем .

import asyncio

def background(f):
    def wrapped(*args, **kwargs):
        return asyncio.get_event_loop().run_in_executor(None, f, *args, **kwargs)

    return wrapped

@background
def your_function(argument):
    #code

Тепер ця функція буде виконуватися паралельно щоразу, коли викликається, не переводячи основну програму в стан очікування. Ви можете використовувати його для паралелізації для циклу. Коли виклик циклу for, цикл є послідовним, але кожна ітерація працює паралельно основній програмі, як тільки інтерпретатор потрапляє туди. Наприклад:

@background
def your_function(argument):
    time.sleep(5)
    print('function finished for '+str(argument))


for i in range(10):
    your_function(i)


print('loop finished')

Це дає наступний вихід:

loop finished
function finished for 4
function finished for 8
function finished for 0
function finished for 3
function finished for 6
function finished for 2
function finished for 5
function finished for 7
function finished for 9
function finished for 1

Я думаю, що є помилка wrapped()**kwargs*kwargs
друку,

На жаль! Моя помилка. Виправлено!
User5

6

чому ви не використовуєте нитки та один мютекс для захисту одного глобального списку?

import os
import re
import time
import sys
import thread

from threading import Thread

class thread_it(Thread):
    def __init__ (self,param):
        Thread.__init__(self)
        self.param = param
    def run(self):
        mutex.acquire()
        output.append(calc_stuff(self.param))
        mutex.release()   


threads = []
output = []
mutex = thread.allocate_lock()

for j in range(0, 10):
    current = thread_it(j * offset)
    threads.append(current)
    current.start()

for t in threads:
    t.join()

#here you have output list filled with data

майте на увазі, ви пройдете так само швидко, як і ваша найповільніша нитка


2
Я знаю, що це дуже стара відповідь, тому обмінятись з нізвідки нецікавим чином. Я лише виступаю за те, що нитки нічого не паралелізують. Нитки в Python прив’язані лише до одного потоку, який виконує інтерпретатор одночасно через загальний замок інтерпретатора, тому вони підтримують одночасне програмування, але не паралельно тому, як вимагає ОП.
skrrgwasme

3
@skrrgwasme Я знаю, що ви це знаєте, але коли ви використовуєте слова "вони нічого не паралелізують", це може ввести читачів у оману. Якщо операції зайнятимуть багато часу, оскільки вони пов'язані з IO або сплять, поки вони чекають події, то інтерпретатор звільняється від запуску інших потоків, тож це призведе до збільшення швидкості, на яку люди сподіваються в цих випадках. Тільки пов'язані з процесором потоки дійсно впливають на те, що говорить skrrgwasme.
Джонатан Хартлі

5

Я виявив, що мені joblibдуже корисно. Дивіться наступний приклад:

from joblib import Parallel, delayed
def yourfunction(k):   
    s=3.14*k*k
    print "Area of a circle with a radius ", k, " is:", s

element_run = Parallel(n_jobs=-1)(delayed(yourfunction)(k) for k in range(1,10))

n_jobs = -1: використовувати всі доступні ядра


14
Знаєте, краще перевірити вже наявні відповіді, перш ніж публікувати свої. Ця відповідь також пропонується використовувати joblib.
sanyash

2

Скажімо, у нас є функція асинхронізації

async def work_async(self, student_name: str, code: str, loop):
"""
Some async function
"""
    # Do some async procesing    

Це потрібно запустити на великому масиві. Деякі атрибути передаються програмі, а деякі використовуються із властивості елемента словника у масиві.

async def process_students(self, student_name: str, loop):
    market = sys.argv[2]
    subjects = [...] #Some large array
    batchsize = 5
    for i in range(0, len(subjects), batchsize):
        batch = subjects[i:i+batchsize]
        await asyncio.gather(*(self.work_async(student_name,
                                           sub['Code'],
                                           loop)
                       for sub in batch))

1

Погляньте на це;

http://docs.python.org/library/queue.html

Це може бути не правильним способом зробити це, але я б зробив щось подібне;

Фактичний код;

from multiprocessing import Process, JoinableQueue as Queue 

class CustomWorker(Process):
    def __init__(self,workQueue, out1,out2,out3):
        Process.__init__(self)
        self.input=workQueue
        self.out1=out1
        self.out2=out2
        self.out3=out3
    def run(self):
            while True:
                try:
                    value = self.input.get()
                    #value modifier
                    temp1,temp2,temp3 = self.calc_stuff(value)
                    self.out1.put(temp1)
                    self.out2.put(temp2)
                    self.out3.put(temp3)
                    self.input.task_done()
                except Queue.Empty:
                    return
                   #Catch things better here
    def calc_stuff(self,param):
        out1 = param * 2
        out2 = param * 4
        out3 = param * 8
        return out1,out2,out3
def Main():
    inputQueue = Queue()
    for i in range(10):
        inputQueue.put(i)
    out1 = Queue()
    out2 = Queue()
    out3 = Queue()
    processes = []
    for x in range(2):
          p = CustomWorker(inputQueue,out1,out2,out3)
          p.daemon = True
          p.start()
          processes.append(p)
    inputQueue.join()
    while(not out1.empty()):
        print out1.get()
        print out2.get()
        print out3.get()
if __name__ == '__main__':
    Main()

Сподіваюся, що це допомагає.


1

Це може бути корисно при впровадженні багатопроцесорних та паралельних / розподілених обчислень в Python.

Підручник YouTube щодо використання пакета techila

Techila - це розподілене обчислювальне проміжне програмне забезпечення, яке інтегрується безпосередньо з Python за допомогою пакета techila. Функція персика в упаковці може бути корисною для паралелізації структури циклів. (Наступний фрагмент коду знаходиться з форумів спільноти Techila )

techila.peach(funcname = 'theheavyalgorithm', # Function that will be called on the compute nodes/ Workers
    files = 'theheavyalgorithm.py', # Python-file that will be sourced on Workers
    jobs = jobcount # Number of Jobs in the Project
    )

1
Хоча це посилання може відповісти на питання, краще включити сюди суттєві частини відповіді та надати посилання для довідки. Відповіді лише на посилання можуть стати недійсними, якщо пов’язана сторінка зміниться.
SL Barth - Відновіть Моніку

2
@SLBarth дякую за відгук. Я додав невеликий зразок коду до відповіді.
TEE

1

дякую @iuryxavier

from multiprocessing import Pool
from multiprocessing import cpu_count


def add_1(x):
    return x + 1

if __name__ == "__main__":
    pool = Pool(cpu_count())
    results = pool.map(add_1, range(10**12))
    pool.close()  # 'TERM'
    pool.join()   # 'KILL'

2
-1. Це відповідь лише для коду. Я б запропонував додати пояснення, яке повідомляє читачам про те, що робить опублікований вами код та, можливо, де вони можуть знайти додаткову інформацію.
starbeamrainbowlabs

-1

дуже простий приклад паралельної обробки

from multiprocessing import Process

output1 = list()
output2 = list()
output3 = list()

def yourfunction():
    for j in range(0, 10):
        # calc individual parameter value
        parameter = j * offset
        # call the calculation
        out1, out2, out3 = calc_stuff(parameter=parameter)

        # put results into correct output list
        output1.append(out1)
        output2.append(out2)
        output3.append(out3)

if __name__ == '__main__':
    p = Process(target=pa.yourfunction, args=('bob',))
    p.start()
    p.join()

3
Тут немає паралелізму в циклі for, ви просто нерестуєте процес, який запускає весь цикл; це НЕ те, що планувала ОП.
facuq
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.