Đồng thời.futures vs Multiprocessing trong Python 3


148

Python 3.2 đã giới thiệu Tương lai đồng thời , dường như là một sự kết hợp nâng cao của các mô đun xử lý luồng và đa xử lý cũ hơn .

Những lợi thế và bất lợi của việc sử dụng điều này cho các nhiệm vụ ràng buộc CPU so với mô-đun đa xử lý cũ là gì?

Bài viết này cho thấy họ làm việc dễ dàng hơn nhiều - có phải vậy không?

Câu trả lời:


145

Tôi sẽ không gọi concurrent.futures"nâng cao" hơn - đó là một giao diện đơn giản hoạt động rất giống nhau bất kể bạn sử dụng nhiều luồng hay nhiều tiến trình như là mánh lới quảng cáo song song cơ bản.

Vì vậy, giống như hầu hết tất cả các trường hợp của "giao diện đơn giản hơn", có rất nhiều sự đánh đổi tương tự: nó có đường cong học tập nông hơn, phần lớn chỉ vì có rất ít thứ để học; nhưng, vì nó cung cấp ít tùy chọn hơn, cuối cùng nó có thể làm bạn thất vọng theo cách mà các giao diện phong phú hơn sẽ không.

Theo như các nhiệm vụ liên quan đến CPU, thì đó là cách quá ít để nói có ý nghĩa nhiều. Đối với các tác vụ gắn với CPU trong CPython, bạn cần nhiều quy trình thay vì nhiều luồng để có bất kỳ cơ hội nào để tăng tốc. Nhưng mức độ tăng tốc (nếu có) của bạn tăng bao nhiêu tùy thuộc vào chi tiết về phần cứng, hệ điều hành của bạn và đặc biệt là mức độ liên lạc giữa các quá trình mà các tác vụ cụ thể của bạn yêu cầu. Trong vỏ bọc, tất cả các mánh lới song song hóa giữa các quá trình đều dựa trên cùng các nguyên hàm HĐH - API cấp cao mà bạn sử dụng để có được những yếu tố không phải là yếu tố chính trong tốc độ dòng dưới cùng.

Chỉnh sửa: ví dụ

Đây là mã cuối cùng được hiển thị trong bài viết mà bạn đã tham chiếu, nhưng tôi đang thêm một câu lệnh nhập cần thiết để làm cho nó hoạt động:

from concurrent.futures import ProcessPoolExecutor
def pool_factorizer_map(nums, nprocs):
    # Let the executor divide the work among processes by using 'map'.
    with ProcessPoolExecutor(max_workers=nprocs) as executor:
        return {num:factors for num, factors in
                                zip(nums,
                                    executor.map(factorize_naive, nums))}

Đây chính xác là điều tương tự bằng cách sử dụng multiprocessingthay thế:

import multiprocessing as mp
def mp_factorizer_map(nums, nprocs):
    with mp.Pool(nprocs) as pool:
        return {num:factors for num, factors in
                                zip(nums,
                                    pool.map(factorize_naive, nums))}

Lưu ý rằng khả năng sử dụng multiprocessing.Poolcác đối tượng làm trình quản lý bối cảnh đã được thêm vào Python 3.3.

Cái nào dễ làm việc hơn? LOL ;-) Về cơ bản chúng giống hệt nhau.

Một điểm khác biệt là nó Poolhỗ trợ rất nhiều cách làm việc khác nhau mà bạn có thể không nhận ra nó có thể dễ dàng như thế nào cho đến khi bạn leo lên một cách hoàn toàn trên đường cong học tập.

Một lần nữa, tất cả những cách khác nhau vừa là điểm mạnh vừa là điểm yếu. Chúng là một thế mạnh bởi vì sự linh hoạt có thể được yêu cầu trong một số tình huống. Chúng là một điểm yếu vì "tốt nhất chỉ có một cách rõ ràng để làm điều đó". Một dự án gắn bó riêng (nếu có thể) concurrent.futurescó thể sẽ dễ dàng duy trì hơn trong thời gian dài, do thiếu tính mới lạ vô cớ trong cách sử dụng API tối thiểu của nó.


20
"Bạn cần nhiều quy trình thay vì nhiều luồng để có cơ hội tăng tốc" là quá khắc nghiệt. Nếu tốc độ là quan trọng; mã có thể đã sử dụng thư viện C và do đó nó có thể giải phóng GIL, ví dụ: regex, lxml, numpy.
jfs

4
@JFSebastian, cảm ơn vì đã thêm điều đó - có lẽ tôi nên nói "theo CPython thuần túy ", nhưng tôi sợ không có cách nào ngắn để giải thích sự thật ở đây mà không thảo luận về GIL.
Tim Peters

2
Và điều đáng nói là các luồng có thể đặc biệt hữu ích và đủ khi hoạt động với IO dài.
kotrfa

9
@TimPeters Trong một số cách ProcessPoolExecutorthực sự có nhiều lựa chọn hơn PoolProcessPoolExecutor.submitlợi nhuận Futurehợp cho phép hủy bỏ ( cancel), kiểm tra ngoại lệ được nâng lên ( exception), và tự động thêm một callback được gọi khi hoàn thành ( add_done_callback). Không có tính năng nào trong số này có sẵn với các AsyncResultphiên bản được trả về Pool.apply_async. Theo những cách khác Poolcó nhiều lựa chọn hơn do initializer/ initargs, maxtasksperchildcontexttrong Pool.__init__, và nhiều hơn nữa phương pháp tiếp xúc bằng Poolví dụ.
tối đa

2
@max, chắc chắn, nhưng lưu ý rằng câu hỏi không phải là về Pool, đó là về các mô-đun. Poollà một phần nhỏ của những gì trong đó multiprocessing, và đến nay trong các tài liệu phải mất một thời gian để mọi người nhận ra nó thậm chí còn tồn tại multiprocessing. Câu trả lời đặc biệt này tập trung vào Poolvì đó là tất cả bài viết mà OP liên kết với đã sử dụng và đó cflà "dễ dàng hơn để làm việc với" đơn giản là không đúng về những gì bài báo đã thảo luận. Ngoài ra, cfas_completed()cũng có thể rất tiện dụng.
Tim Peters
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.