Làm thế nào để lập trình song song trong Python?


141

Đối với C ++, chúng ta có thể sử dụng OpenMP để lập trình song song; tuy nhiên, OpenMP sẽ không hoạt động đối với Python. Tôi nên làm gì nếu tôi muốn song song một số phần trong chương trình python của mình?

Cấu trúc của mã có thể được coi là:

solve1(A)
solve2(B)

Ở đâu solve1solve2là hai chức năng độc lập. Làm thế nào để chạy song song loại mã này thay vì theo trình tự để giảm thời gian chạy? Hy vọng ai đó có thể giúp tôi. Cảm ơn rất nhiều trước. Mã này là:

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

Trong đó setinner và setouter là hai hàm độc lập. Đó là nơi tôi muốn song song ...


31
Hãy nhìn vào đa xử lý . Lưu ý: Các luồng của Python không phù hợp với các tác vụ gắn với CPU, chỉ dành cho ràng buộc I / O.
9000

4
@ 9000 +100 internets để đề cập đến các tác vụ phụ thuộc CPU và I / O.
Hyperboreus

@ 9000 Trên thực tế, các luồng không phù hợp với nhiệm vụ gắn với CPU theo như tôi biết! Các quy trình là cách để đi khi thực hiện các tác vụ thực sự gắn với CPU.
Omar Al-Ithawi

6
@OmarIthawi: tại sao, các luồng hoạt động tốt nếu bạn có nhiều lõi CPU (như bình thường bây giờ). Sau đó, quy trình của bạn có thể chạy song song một số luồng tải tất cả các lõi này chia sẻ dữ liệu chung giữa chúng (nghĩa là không có vùng nhớ chia sẻ rõ ràng hoặc nhắn tin liên tiến trình).
9000

1
@ user2134774: Vâng, vâng, nhận xét thứ hai của tôi không có ý nghĩa gì. Có lẽ các phần mở rộng C duy nhất phát hành GIL có thể hưởng lợi từ đó; ví dụ như các bộ phận của NumPy và Pandas làm điều đó. Trong các trường hợp khác, đó là sai (nhưng tôi không thể chỉnh sửa nó ngay bây giờ).
9000

Câu trả lời:


162

Bạn có thể sử dụng mô-đun đa xử lý . Trong trường hợp này, tôi có thể sử dụng nhóm xử lý:

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)

Điều này sẽ sinh ra các quy trình có thể làm công việc chung cho bạn. Vì chúng tôi không vượt qua processes, nó sẽ sinh ra một quy trình cho mỗi lõi CPU trên máy của bạn. Mỗi lõi CPU có thể thực hiện đồng thời một tiến trình.

Nếu bạn muốn ánh xạ một danh sách tới một chức năng duy nhất, bạn sẽ làm điều này:

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

Không sử dụng các luồng vì GIL khóa mọi hoạt động trên các đối tượng python.


1
không pool.mapcòn chấp nhận các từ điển như args? Hay chỉ những danh sách đơn giản?
Bndr

Chỉ cần liệt kê tôi nghĩ. Nhưng bạn chỉ có thể vượt qua trong dict.items () sẽ là danh sách các bộ dữ liệu giá trị chính
Matt Williamson

Thật không may, điều này kết thúc bằng một loại 'không thể xóa được: lỗi' list'`
Bndr

ngoài nhận xét cuối cùng của tôi: `dict.items ()` làm việc. Lỗi tăng lên, bởi vì tôi đã phải thay đổi việc xử lý biến để hiểu rõ quá trình - funktion. Thật không may, thông báo lỗi không hữu ích lắm ... Vì vậy: cảm ơn bạn đã gợi ý. :-)
Bndr

2
Thời gian chờ ở đây là gì?
gamma

26

Điều này có thể được thực hiện rất thanh lịch với Ray .

Để song song hóa ví dụ của bạn, bạn cần xác định các chức năng của mình với trình @ray.remotetrang trí và sau đó gọi chúng bằng .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])

Có một số lợi thế của điều này so với mô đun đa xử lý .

  1. Mã tương tự sẽ chạy trên một máy đa lõi cũng như một cụm máy.
  2. Các quy trình chia sẻ dữ liệu hiệu quả thông qua bộ nhớ chia sẻ và tuần tự hóa không sao chép .
  3. Thông báo lỗi được tuyên truyền độc đáo.
  4. Các lệnh gọi hàm này có thể được tạo thành cùng nhau, ví dụ:

    @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. Ngoài việc gọi các chức năng từ xa, các lớp có thể được khởi tạo từ xa như các tác nhân .

Lưu ý rằng Ray là một khung mà tôi đã giúp phát triển.


Tôi liên tục nhận được thông báo "Không thể tìm thấy phiên bản thỏa mãn tia yêu cầu (từ phiên bản :) Không tìm thấy phân phối phù hợp cho tia" khi cố gắng cài đặt gói trong python
alwaysaskingquestions 16/2/18

2
Thông thường loại lỗi này có nghĩa là bạn cần nâng cấp pip. Tôi khuyên bạn nên thử pip install --upgrade pip. Nếu bạn cần sử dụng sudotất cả thì có thể phiên bản pipmà bạn đang sử dụng để cài đặt raykhông giống với phiên bản đang được nâng cấp. Bạn có thể kiểm tra với pip --version. Ngoài ra, Windows hiện không được hỗ trợ vì vậy nếu bạn ở trên Windows có lẽ là vấn đề.
Robert Nishihara

1
Chỉ cần lưu ý điều này chủ yếu để phân phối công việc đồng thời trên nhiều máy.
Matt Williamson

2
Nó thực sự được tối ưu hóa cho cả vỏ máy đơn và cài đặt cụm. Rất nhiều quyết định thiết kế (ví dụ, bộ nhớ dùng chung, tuần tự hóa không sao chép) được nhắm mục tiêu hỗ trợ tốt cho các máy đơn lẻ.
Robert Nishihara

2
Sẽ thật tuyệt nếu các tài liệu chỉ ra điều đó nhiều hơn. Tôi có cảm giác khi đọc qua các tài liệu rằng nó không thực sự dành cho trường hợp máy đơn lẻ.
Sledge


4

Giải pháp, như những người khác đã nói, là sử dụng nhiều quy trình. Khung nào phù hợp hơn, tuy nhiên, phụ thuộc vào nhiều yếu tố. Ngoài những cái đã được đề cập, còn có charm4pympi4py (Tôi là nhà phát triển của charm4py).

Có một cách hiệu quả hơn để thực hiện ví dụ trên hơn là sử dụng trừu tượng nhóm công nhân. Vòng lặp chính sẽ gửi cùng một tham số (bao gồm cả biểu đồ hoàn chỉnh G) cho các công nhân trong mỗi 1000 lần lặp. Vì ít nhất một công nhân sẽ cư trú trên một quy trình khác, điều này liên quan đến việc sao chép và gửi các đối số cho (các) quy trình khác. Điều này có thể rất tốn kém tùy thuộc vào kích thước của các đối tượng. Thay vào đó, nó có ý nghĩa để có công nhân lưu trữ trạng thái và chỉ cần gửi thông tin cập nhật.

Ví dụ, trong charm4py, điều này có thể được thực hiện như thế này:

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

Lưu ý rằng đối với ví dụ này, chúng tôi thực sự chỉ cần một công nhân. Vòng lặp chính có thể thực thi một trong các hàm và có nhân viên thực thi hàm kia. Nhưng mã của tôi giúp minh họa một vài điều:

  1. Công nhân A chạy trong tiến trình 0 (giống như vòng lặp chính). Trong khi result_a.get()bị chặn chờ kết quả, công nhân A thực hiện tính toán trong cùng một quy trình.
  2. Các đối số được tự động chuyển qua tham chiếu đến công nhân A, vì nó nằm trong cùng một quy trình (không có sao chép liên quan).

2

Trong một số trường hợp, có thể tự động song song các vòng lặp bằng Numba , mặc dù nó chỉ hoạt động với một tập hợp nhỏ của 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

Thật không may, có vẻ như Numba chỉ hoạt động với các mảng Numpy chứ không hoạt động với các đối tượng Python khác. Về lý thuyết, cũng có thể biên dịch Python thành C ++ và sau đó tự động song song hóa nó bằng trình biên dịch Intel C ++ , mặc dù tôi chưa thử điều này.


2

Bạn có thể sử dụng joblibthư viện để thực hiện tính toán song song và đa xử lý.

from joblib import Parallel, delayed

Bạn có thể chỉ cần tạo một hàm foomà bạn muốn được chạy song song và dựa trên đoạn mã sau đây thực hiện xử lý song song:

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

Nơi num_corescó thể được lấy từ multiprocessingthư viện như sau:

import multiprocessing

num_cores = multiprocessing.cpu_count()

Nếu bạn có một hàm có nhiều hơn một đối số đầu vào và bạn chỉ muốn lặp lại một trong các đối số theo danh sách, bạn có thể sử dụng partialhàm từ functoolsthư viện như sau:

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)

Bạn có thể tìm thấy một lời giải thích đầy đủ về đa xử lý python và R với một vài ví dụ ở đây .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.