Як зробити паралельне програмування в Python?


141

Для C ++ ми можемо використовувати OpenMP для паралельного програмування; проте OpenMP не працюватиме для Python. Що робити, якщо я хочу паралельно деяким частинам своєї програми python?

Структура коду може розглядатися як:

solve1(A)
solve2(B)

Де solve1і solve2є дві незалежні функції. Як запустити цей вид коду паралельно, а не послідовно, щоб скоротити час роботи? Сподіваюся, хтось може мені допомогти. Дякую заздалегідь. Код:

def solve(Q, G, n):
    i = 0
    tol = 10 ** -4

    while i < 1000:
        inneropt, partition, x = setinner(Q, G, n)
        outeropt = setouter(Q, G, n)

        if (outeropt - inneropt) / (1 + abs(outeropt) + abs(inneropt)) < tol:
            break

        node1 = partition[0]
        node2 = partition[1]

        G = updateGraph(G, node1, node2)

        if i == 999:
            print "Maximum iteration reaches"
    print inneropt

Там, де сетнер і сетутер - дві незалежні функції. Ось де я хочу паралельно ...


31
Погляньте на багатообробку . Примітка. Нитки Python не підходять для завдань, пов'язаних з процесором, лише для введення-виводу.
9000

4
@ 9000 +100 інтернетів для згадування завдань, залежних від процесора та вводу / виводу.
Гіперборей

@ 9000 Насправді потоки взагалі не підходять для пов'язаних з процесором завдань, наскільки я знаю! Процеси - це шлях до виконання реальних завдань, пов'язаних з процесором.
Омар Аль-Ітаві

6
@OmarIthawi: чому, теми працюють добре, якщо у вас багато ядер процесора (як зазвичай). Тоді ваш процес може запускати декілька потоків, завантажуючи всі ці ядра паралельно та обмінюючись загальними даними між ними неявно (тобто без явної області спільної пам’яті чи міжпроцесорних повідомлень).
9000

1
@ user2134774: Ну, так, мій другий коментар мало сенсу. Мабуть, від цього можуть отримати користь лише розширення C, які вивільняють GIL; наприклад, це роблять частини NumPy і Pandas. В інших випадках це неправильно (але я зараз не можу його редагувати).
9000

Відповіді:


162

Можна використовувати багатопроцесорний модуль. У цьому випадку я можу використовувати пул обробки:

from multiprocessing import Pool
pool = Pool()
result1 = pool.apply_async(solve1, [A])    # evaluate "solve1(A)" asynchronously
result2 = pool.apply_async(solve2, [B])    # evaluate "solve2(B)" asynchronously
answer1 = result1.get(timeout=10)
answer2 = result2.get(timeout=10)

Це дозволить породити процеси, які можуть зробити загальну роботу для вас. Оскільки ми не пройшли processes, він породжує один процес для кожного ядра процесора на вашій машині. Кожне ядро ​​CPU може виконувати один процес одночасно.

Якщо ви хочете зіставити список з однією функцією, зробіть це так:

args = [A, B]
results = pool.map(solve1, args)

Не використовуйте потоки, оскільки GIL блокує будь-які операції на об'єктах python.


1
чи pool.mapприймає словники як аргументи? Або лише прості списки?
Бндр

Просто списки, я думаю. Але ви можете просто передати dict.items (), який буде переліком ключових значень кортежів
Метт Вільямсон

На жаль, це закінчується помилкою "unhashable type: 'list'"
The Bndr

на додачу до мого останнього коментаря: робота dict.items (). Помилка піднімається, тому що мені довелося змінити керованість змінної розуміння процесу-функції. На жаль, повідомлення про помилку було не дуже корисним ... Отже: дякую за підказку. :-)
The Bndr

2
Який тут час очікування?
гамма

26

З Реєм це можна зробити дуже елегантно .

Щоб паралелізувати свій приклад, вам потрібно визначити свої функції з @ray.remoteдекоратором, а потім викликати їх .remote.

import ray

ray.init()

# Define the functions.

@ray.remote
def solve1(a):
    return 1

@ray.remote
def solve2(b):
    return 2

# Start two tasks in the background.
x_id = solve1.remote(0)
y_id = solve2.remote(1)

# Block until the tasks are done and get the results.
x, y = ray.get([x_id, y_id])

Є ряд переваг цього перед багатопроцесорним модулем.

  1. Цей же код буде працювати на багатоядерній машині, а також на кластері машин.
  2. Процеси ефективно обмінюються даними завдяки спільній пам’яті та серіалізації з нульовою копією .
  3. Повідомлення про помилки добре поширюються.
  4. Ці виклики функцій можуть бути складені разом, наприклад,

    @ray.remote
    def f(x):
        return x + 1
    
    x_id = f.remote(1)
    y_id = f.remote(x_id)
    z_id = f.remote(y_id)
    ray.get(z_id)  # returns 4
  5. Окрім віддаленого виклику функцій, класи можна віддалено інстанціювати як акторів .

Зауважте, що Рей - це основа, якій я допомагав розвиватися.


я продовжую отримувати помилку, яка говорить "Не вдалося знайти версію, яка б задовольняла промінь (від версій:) Не знайдено відповідного розподілу для ray" при спробі встановити пакет у python
завждизапитання

2
Зазвичай така помилка означає, що вам потрібно оновити pip. Я б запропонував спробувати pip install --upgrade pip. Якщо вам потрібно sudoвзагалі користуватися, то можливо, що версія pip, яку ви використовуєте для встановлення ray, не є тією, що оновлюється. Ви можете перевірити pip --version. Також Windows зараз не підтримується, тому, якщо ви працюєте в Windows, це, мабуть, проблема.
Роберт Нішихара

1
Лише зауважте, що це насамперед для розповсюдження одночасних завдань на декількох машинах.
Метт Вільямсон

2
Він фактично оптимізований як для одномашинного корпусу, так і для кластерної настройки. Багато дизайнерських рішень (наприклад, спільна пам'ять, серіалізація з нульовою копією) спрямовані на те, щоб добре підтримувати окремі машини.
Роберт Нішіхара

2
Було б чудово, якби доктори вказали на це більше. Я почув, читаючи документи, що це насправді не призначено для єдиного корпусу машини.
Санки

4

CPython використовує Global Interpreter Lock, що робить паралельне програмування трохи цікавішим, ніж C ++

У цій темі є кілька корисних прикладів та описів виклику:

Проблема Python Global Interpreter Lock (GIL) для багатоядерних систем з використанням набору завдань для Linux?


13
Ви називаєте нездатність дійсно запустити код одночасно "цікавим"? : - /
ManuelSchneid3r

4

Рішення, як говорили інші, полягає у використанні декількох процесів. Яка структура є більш підходящою, проте залежить від багатьох факторів. Окрім згаданих, є також charm4py та mpi4py (я розробник charm4py).

Існує більш ефективний спосіб втілити вищевказаний приклад, ніж використання абстракції пулу працівників. Основний цикл надсилає ті ж параметри (включаючи повний графік G) знову і знову робочим у кожному з 1000 ітерацій. Оскільки принаймні один працівник буде проживати в іншому процесі, це включає копіювання та надсилання аргументів іншим процесам. Це може бути дуже дорогим залежно від розміру об'єктів. Натомість, має сенс працівники зберігати стан та просто надсилати оновлену інформацію.

Наприклад, у charm4py це можна зробити так:

class Worker(Chare):

    def __init__(self, Q, G, n):
        self.G = G
        ...

    def setinner(self, node1, node2):
        self.updateGraph(node1, node2)
        ...


def solve(Q, G, n):
    # create 2 workers, each on a different process, passing the initial state
    worker_a = Chare(Worker, onPE=0, args=[Q, G, n])
    worker_b = Chare(Worker, onPE=1, args=[Q, G, n])
    while i < 1000:
        result_a = worker_a.setinner(node1, node2, ret=True)  # execute setinner on worker A
        result_b = worker_b.setouter(node1, node2, ret=True)  # execute setouter on worker B

        inneropt, partition, x = result_a.get()  # wait for result from worker A
        outeropt = result_b.get()  # wait for result from worker B
        ...

Зауважте, що для цього прикладу нам справді потрібен лише один працівник. Основний цикл може виконувати одну з функцій, а працівник - виконувати іншу. Але мій код допомагає проілюструвати пару речей:

  1. Worker A запускає процес 0 (те саме, що і основний цикл). Поки result_a.get()блокується очікування результату, робітник A здійснює обчислення в тому ж процесі.
  2. Аргументи автоматично передаються посиланням на працівника А, оскільки він перебуває в тому самому процесі (копіювання не бере участь).

2

У деяких випадках можливо автоматично паралелізувати цикли за допомогою Numba , хоча це працює лише з невеликим підмножиною Python:

from numba import njit, prange

@njit(parallel=True)
def prange_test(A):
    s = 0
    # Without "parallel=True" in the jit-decorator
    # the prange statement is equivalent to range
    for i in prange(A.shape[0]):
        s += A[i]
    return s

На жаль, здається, що Numba працює лише з Numpy-масивами, але не з іншими об’єктами Python. Теоретично, можливо, також можливо скомпілювати Python до C ++, а потім автоматично паралелізувати його за допомогою компілятора Intel C ++ , хоча я ще цього не пробував.


2

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

from joblib import Parallel, delayed

Ви можете просто створити функцію, fooяку потрібно запустити паралельно і на основі наступного фрагмента коду реалізувати паралельну обробку:

output = Parallel(n_jobs=num_cores)(delayed(foo)(i) for i in input)

Де num_coresможна отримати з multiprocessingбібліотеки наступне:

import multiprocessing

num_cores = multiprocessing.cpu_count()

Якщо у вас є функція з більш ніж одним вхідним аргументом, і ви просто хочете повторити один з аргументів списком, ви можете використовувати partialфункцію з functoolsбібліотеки наступним чином:

from joblib import Parallel, delayed
import multiprocessing
from functools import partial
def foo(arg1, arg2, arg3, arg4):
    '''
    body of the function
    '''
    return output
input = [11,32,44,55,23,0,100,...] # arbitrary list
num_cores = multiprocessing.cpu_count()
foo_ = partial(foo, arg2=arg2, arg3=arg3, arg4=arg4)
# arg1 is being fetched from input list
output = Parallel(n_jobs=num_cores)(delayed(foo_)(i) for i in input)

Ви можете знайти повне пояснення пітона і R багатопроцесорних з парою прикладів тут .

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