Những gì Giulio Franco nói là đúng cho đa luồng so với đa xử lý nói chung .
Tuy nhiên, Python * có một vấn đề được thêm vào: Có Khóa phiên dịch toàn cầu ngăn hai luồng trong cùng một quy trình chạy mã Python cùng một lúc. Điều này có nghĩa là nếu bạn có 8 lõi và thay đổi mã của mình để sử dụng 8 luồng, nó sẽ không thể sử dụng CPU 800% và chạy nhanh hơn 8 lần; nó sẽ sử dụng cùng CPU 100% và chạy ở cùng tốc độ. (Trong thực tế, nó sẽ chạy chậm hơn một chút, vì có thêm chi phí từ luồng, ngay cả khi bạn không có bất kỳ dữ liệu chia sẻ nào, nhưng bỏ qua điều đó ngay bây giờ.)
Có nhiều ngoại lệ cho cái này. Nếu tính toán nặng nề của mã của bạn không thực sự xảy ra trong Python, nhưng trong một số thư viện có mã C tùy chỉnh xử lý GIL đúng cách, như ứng dụng gọn gàng, bạn sẽ nhận được lợi ích hiệu suất mong đợi từ luồng. Điều tương tự cũng đúng nếu tính toán nặng được thực hiện bởi một số quy trình con mà bạn chạy và chờ đợi.
Quan trọng hơn, có những trường hợp điều này không quan trọng. Ví dụ: một máy chủ mạng dành phần lớn thời gian để đọc các gói ra khỏi mạng và một ứng dụng GUI dành phần lớn thời gian để chờ đợi các sự kiện của người dùng. Một lý do để sử dụng các luồng trong máy chủ mạng hoặc ứng dụng GUI là cho phép bạn thực hiện các "tác vụ nền" chạy dài mà không ngăn luồng chính tiếp tục dịch vụ gói mạng hoặc sự kiện GUI. Và nó hoạt động tốt với các chủ đề Python. (Về mặt kỹ thuật, điều này có nghĩa là các luồng Python cung cấp cho bạn sự tương tranh, mặc dù chúng không cung cấp cho bạn tính song song cốt lõi.)
Nhưng nếu bạn đang viết một chương trình gắn với CPU bằng Python thuần túy, sử dụng nhiều luồng hơn thường không hữu ích.
Sử dụng các quy trình riêng biệt không có vấn đề như vậy với GIL, vì mỗi quy trình có GIL riêng. Tất nhiên, bạn vẫn có tất cả sự đánh đổi giữa các luồng và quy trình như trong bất kỳ ngôn ngữ nào khác. Việc chia sẻ dữ liệu giữa các tiến trình khó hơn và tốn kém hơn giữa các luồng, có thể tốn kém khi chạy một số lượng lớn các quy trình hoặc để tạo và hủy chúng thường xuyên, v.v. Nhưng GIL cân nhắc rất nhiều về sự cân bằng đối với các quy trình, theo cách không đúng với C, Java. Vì vậy, bạn sẽ thấy mình sử dụng đa xử lý thường xuyên hơn trong Python so với trong C hoặc Java.
Trong khi đó, triết lý "bao gồm pin" của Python mang đến một tin tốt: Viết mã rất dễ dàng có thể được chuyển đổi qua lại giữa các luồng và quy trình với thay đổi một lớp.
Nếu bạn thiết kế mã theo các "công việc" khép kín không chia sẻ bất cứ điều gì với các công việc khác (hoặc chương trình chính) ngoại trừ đầu vào và đầu ra, bạn có thể sử dụng concurrent.futures
thư viện để viết mã của mình xung quanh nhóm luồng như thế này:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
executor.submit(job, argument)
executor.map(some_function, collection_of_independent_things)
# ...
Bạn thậm chí có thể nhận được kết quả của những công việc đó và chuyển chúng sang các công việc tiếp theo, chờ đợi mọi thứ theo thứ tự thực hiện hoặc theo thứ tự hoàn thành, v.v.; đọc phần trên Future
các đối tượng để biết chi tiết.
Bây giờ, nếu hóa ra chương trình của bạn liên tục sử dụng CPU 100% và việc thêm nhiều luồng chỉ làm cho nó chậm hơn, thì bạn đang gặp vấn đề về GIL, vì vậy bạn cần chuyển sang các quy trình. Tất cả bạn phải làm là thay đổi dòng đầu tiên:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
Nhắc nhở thực sự duy nhất là các đối số và giá trị trả về của công việc của bạn phải có thể được chọn (và không mất quá nhiều thời gian hoặc bộ nhớ để xử lý) để có thể xử lý chéo. Thông thường đây không phải là một vấn đề, nhưng đôi khi nó là.
Nhưng nếu công việc của bạn không thể khép kín thì sao? Nếu bạn có thể thiết kế mã của mình theo các công việc chuyển thông điệp từ người này sang người khác, điều đó vẫn khá dễ dàng. Bạn có thể phải sử dụng threading.Thread
hoặc multiprocessing.Process
thay vì dựa vào hồ bơi. Và bạn sẽ phải tạo queue.Queue
hoặc multiprocessing.Queue
các đối tượng rõ ràng. (Có rất nhiều tùy chọn khác Các ống, ổ cắm, tập tin có nhiều đàn khác, nhưng vấn đề là, bạn phải làm một cách thủ công nếu phép thuật tự động của Executor không đủ.)
Nhưng nếu bạn thậm chí không thể dựa vào tin nhắn đi qua thì sao? Điều gì xảy ra nếu bạn cần hai công việc để cả hai cùng biến đổi cấu trúc và xem những thay đổi của nhau? Trong trường hợp đó, bạn sẽ cần thực hiện đồng bộ hóa thủ công (khóa, semaphores, điều kiện, v.v.) và, nếu bạn muốn sử dụng các quy trình, các đối tượng bộ nhớ chia sẻ rõ ràng để khởi động. Đây là khi đa luồng (hoặc đa xử lý) gặp khó khăn. Nếu bạn có thể tránh nó, thật tuyệt; nếu bạn không thể, bạn sẽ cần đọc nhiều hơn ai đó có thể đưa vào câu trả lời SO.
Từ một nhận xét, bạn muốn biết có gì khác nhau giữa các luồng và các tiến trình trong Python. Thực sự, nếu bạn đọc câu trả lời của Giulio Franco và của tôi và tất cả các liên kết của chúng tôi, điều đó sẽ bao gồm tất cả mọi thứ, nhưng một bản tóm tắt chắc chắn sẽ hữu ích, vì vậy hãy vào đây:
- Chủ đề chia sẻ dữ liệu theo mặc định; quy trình không.
- Như một hệ quả của (1), việc gửi dữ liệu giữa các quy trình thường đòi hỏi phải xử lý và giải nén nó. **
- Như một hệ quả khác của (1), chia sẻ dữ liệu trực tiếp giữa các quy trình thường yêu cầu đưa nó vào các định dạng cấp thấp như Giá trị, Mảng và
ctypes
các loại.
- Các quy trình không phải chịu GIL.
- Trên một số nền tảng (chủ yếu là Windows), các quy trình tốn kém hơn nhiều để tạo và hủy.
- Có một số hạn chế bổ sung đối với các quy trình, một số trong đó khác nhau trên các nền tảng khác nhau. Xem hướng dẫn lập trình để biết chi tiết.
- Các
threading
mô-đun không có một số tính năng của multiprocessing
module. (Bạn có thể sử dụng multiprocessing.dummy
để có được hầu hết các API bị thiếu trên đầu các chuỗi hoặc bạn có thể sử dụng các mô-đun cấp cao hơn như thế nào concurrent.futures
và không phải lo lắng về nó.)
* Thực ra không phải Python, ngôn ngữ, có vấn đề này, mà là CPython, cách triển khai "chuẩn" của ngôn ngữ đó. Một số triển khai khác không có GIL, như Jython.
** Nếu bạn đang sử dụng phương thức khởi động ngã ba để đa xử lý mà bạn có thể sử dụng trên hầu hết các nền tảng không phải Windows, thì mỗi quy trình con đều nhận được bất kỳ tài nguyên nào mà cha mẹ có khi trẻ bắt đầu, đó có thể là một cách khác để truyền dữ liệu cho trẻ.
Thread
mô-đun (được gọi là_thread
python 3.x). Thành thật mà nói, bản thân tôi chưa bao giờ hiểu sự khác biệt ...