Python có hỗ trợ xử lý song song không?

Nếu bạn đã nghe nhiều cuộc nói chuyện về việc

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 được thêm vào Python nhưng tò mò về cách nó so sánh với các phương thức tương tranh khác hoặc đang tự hỏi đồng thời là gì và nó có thể tăng tốc chương trình của bạn như thế nào, thì bạn đã đến đúng nơi

Trong bài viết này, bạn sẽ học được những điều sau

  • đồng thời là gì
  • song song là gì
  • Cách so sánh một số phương thức tương tranh của Python, bao gồm
    $ ./io_non_concurrent.py
       [most output skipped]
    Downloaded 160 in 14.289619207382202 seconds
    
    7,
    $ ./io_non_concurrent.py
       [most output skipped]
    Downloaded 160 in 14.289619207382202 seconds
    
    6 và
    $ ./io_non_concurrent.py
       [most output skipped]
    Downloaded 160 in 14.289619207382202 seconds
    
    9
  • Khi nào nên sử dụng đồng thời trong chương trình của bạn và sử dụng mô-đun nào

Bài viết này giả định rằng bạn đã có hiểu biết cơ bản về Python và bạn đang sử dụng ít nhất phiên bản 3. 6 để chạy các ví dụ. Bạn có thể tải xuống các ví dụ từ repo Real Python GitHub

Tiền thưởng miễn phí. 5 Suy nghĩ về Làm chủ Python, một khóa học miễn phí dành cho các nhà phát triển Python cho bạn thấy lộ trình và tư duy mà bạn sẽ cần để đưa các kỹ năng Python của mình lên một tầm cao mới

Lấy bài kiểm tra. Kiểm tra kiến ​​thức của bạn với bài kiểm tra tương tác “Python Concurrency” của chúng tôi. Sau khi hoàn thành, bạn sẽ nhận được điểm số để có thể theo dõi quá trình học tập của mình theo thời gian

Lấy bài kiểm tra "

Đồng thời là gì?

Định nghĩa đồng thời trong từ điển là sự xuất hiện đồng thời. Trong Python, những thứ xảy ra đồng thời được gọi bằng các tên khác nhau (luồng, tác vụ, quy trình) nhưng ở cấp độ cao, chúng đều đề cập đến một chuỗi các lệnh chạy theo thứ tự.

Tôi thích nghĩ về chúng như những dòng suy nghĩ khác nhau. Mỗi cái có thể bị dừng tại một số điểm nhất định và CPU hoặc bộ não đang xử lý chúng có thể chuyển sang một điểm khác. Trạng thái của từng cái được lưu để có thể khởi động lại ngay tại nơi nó bị gián đoạn

Bạn có thể thắc mắc tại sao Python sử dụng các từ khác nhau cho cùng một khái niệm. Hóa ra các luồng, nhiệm vụ và quy trình chỉ giống nhau nếu bạn nhìn chúng từ cấp độ cao. Khi bạn bắt đầu tìm hiểu chi tiết, tất cả chúng đều đại diện cho những thứ hơi khác nhau. Bạn sẽ thấy nhiều hơn về sự khác biệt của chúng khi bạn xem qua các ví dụ

Bây giờ hãy nói về phần đồng thời của định nghĩa đó. Bạn phải cẩn thận một chút bởi vì, khi bạn đi sâu vào chi tiết, chỉ có

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9 thực sự chạy những dòng suy nghĩ này cùng một lúc theo đúng nghĩa đen.
import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
1 và
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 đều chạy trên một bộ xử lý và do đó chỉ chạy một bộ tại một thời điểm. Họ chỉ khéo léo tìm cách thay phiên nhau để đẩy nhanh quá trình tổng thể. Mặc dù họ không chạy các dòng suy nghĩ khác nhau đồng thời, chúng tôi vẫn gọi đây là sự đồng thời

Cách các chủ đề hoặc nhiệm vụ thay phiên nhau là sự khác biệt lớn giữa

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7 và
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6. Trong
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7, hệ điều hành thực sự biết về từng luồng và có thể ngắt nó bất kỳ lúc nào để bắt đầu chạy một luồng khác. Đây được gọi là đa nhiệm ưu tiên vì hệ điều hành có thể ưu tiên luồng của bạn để thực hiện chuyển đổi

Đa nhiệm ưu tiên thuận tiện ở chỗ mã trong chuỗi không cần thực hiện bất kỳ thao tác nào để thực hiện chuyển đổi. Nó cũng có thể khó khăn vì cụm từ “bất cứ lúc nào”. Việc chuyển đổi này có thể xảy ra ở giữa một câu lệnh Python, thậm chí là một câu lệnh tầm thường như

import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
6

Mặt khác,

import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
7 sử dụng đa nhiệm hợp tác. Các nhiệm vụ phải hợp tác bằng cách thông báo khi chúng sẵn sàng được tắt. Điều đó có nghĩa là mã trong tác vụ phải thay đổi một chút để thực hiện điều này

Lợi ích của việc làm thêm công việc này trước là bạn luôn biết nhiệm vụ của mình sẽ được hoán đổi ở đâu. Nó sẽ không bị hoán đổi ở giữa câu lệnh Python trừ khi câu lệnh đó được đánh dấu. Sau này bạn sẽ thấy điều này có thể đơn giản hóa các phần trong thiết kế của bạn như thế nào

Loại bỏ các quảng cáo

Song song là gì?

Cho đến giờ, bạn đã xem xét khả năng tương tranh xảy ra trên một bộ xử lý đơn lẻ. Thế còn tất cả các lõi CPU mà chiếc máy tính xách tay mới, tuyệt vời của bạn thì sao?

Với

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9, Python tạo các quy trình mới. Một quy trình ở đây có thể được coi gần như là một chương trình hoàn toàn khác, mặc dù về mặt kỹ thuật, chúng thường được định nghĩa là một tập hợp các tài nguyên trong đó các tài nguyên bao gồm bộ nhớ, xử lý tệp và những thứ tương tự. Một cách để nghĩ về nó là mỗi quy trình chạy trong trình thông dịch Python của riêng nó

Bởi vì chúng là các quy trình khác nhau, mỗi dòng suy nghĩ của bạn trong một chương trình đa xử lý có thể chạy trên một lõi khác nhau. Chạy trên một lõi khác có nghĩa là chúng thực sự có thể chạy cùng lúc, điều này thật tuyệt vời. Có một số phức tạp phát sinh khi thực hiện việc này, nhưng Python thực hiện khá tốt công việc xử lý chúng trong hầu hết thời gian

Bây giờ bạn đã có ý tưởng về đồng thời và song song là gì, hãy xem lại sự khác biệt của chúng và sau đó chúng ta có thể xem tại sao chúng có thể hữu ích

Loại đồng thời Quyết định chuyển đổi Số lượng bộ xử lý Đa nhiệm ưu tiên (

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7) Hệ điều hành quyết định khi nào chuyển đổi các tác vụ bên ngoài sang Python. 1 Đa nhiệm hợp tác (_____ 06)Các nhiệm vụ quyết định khi nào nên từ bỏ quyền kiểm soát. 1 Đa xử lý (________ 09)Tất cả các tiến trình đều chạy đồng thời trên các bộ xử lý khác nhau. Nhiều

Mỗi loại đồng thời này có thể hữu ích. Hãy xem những loại chương trình nào chúng có thể giúp bạn tăng tốc

Đồng thời hữu ích khi nào?

Đồng thời có thể tạo ra sự khác biệt lớn cho hai loại vấn đề. Chúng thường được gọi là giới hạn CPU và giới hạn I/O

Các vấn đề liên quan đến I/O khiến chương trình của bạn chạy chậm lại vì nó thường xuyên phải đợi đầu vào/đầu ra (I/O) từ một số tài nguyên bên ngoài. Chúng phát sinh thường xuyên khi chương trình của bạn đang làm việc với những thứ chậm hơn nhiều so với CPU của bạn

Có rất nhiều ví dụ về những thứ chạy chậm hơn CPU của bạn, nhưng rất may là chương trình của bạn không tương tác với hầu hết chúng. Những thứ chậm mà chương trình của bạn sẽ tương tác thường xuyên nhất là hệ thống tệp và kết nối mạng

Hãy xem nó trông như thế nào

Python có hỗ trợ xử lý song song không?

Trong sơ đồ trên, các hộp màu xanh hiển thị thời gian khi chương trình của bạn đang hoạt động và các hộp màu đỏ là thời gian chờ một thao tác I/O hoàn tất. Sơ đồ này không được mở rộng vì các yêu cầu trên internet có thể mất nhiều thời gian hơn so với các lệnh của CPU, vì vậy chương trình của bạn có thể sẽ dành phần lớn thời gian để chờ đợi. Đây là những gì trình duyệt của bạn đang làm hầu hết thời gian

Mặt khác, có những lớp chương trình thực hiện tính toán quan trọng mà không cần nói chuyện với mạng hoặc truy cập tệp. Đây là các chương trình liên kết với CPU, vì tài nguyên giới hạn tốc độ chương trình của bạn là CPU, không phải mạng hoặc hệ thống tệp

Đây là sơ đồ tương ứng cho chương trình gắn với CPU

Python có hỗ trợ xử lý song song không?

Khi bạn xem qua các ví dụ trong phần sau, bạn sẽ thấy rằng các hình thức đồng thời khác nhau hoạt động tốt hơn hoặc kém hơn với các chương trình gắn kết CPU và I/O. Việc thêm đồng thời vào chương trình của bạn sẽ bổ sung thêm mã và sự phức tạp, vì vậy bạn sẽ cần quyết định xem khả năng tăng tốc có đáng để nỗ lực thêm hay không. Đến cuối bài viết này, bạn sẽ có đủ thông tin để bắt đầu đưa ra quyết định đó

Dưới đây là tóm tắt nhanh để làm rõ khái niệm này

Quy trình giới hạn I/OQuy trình giới hạn CPU Chương trình của bạn dành phần lớn thời gian để nói chuyện với một thiết bị chậm, chẳng hạn như kết nối mạng, ổ cứng hoặc máy in. Chương trình của bạn dành phần lớn thời gian để thực hiện các hoạt động của CPU. Tăng tốc nó liên quan đến việc chồng chéo thời gian chờ đợi các thiết bị này. Tăng tốc nó liên quan đến việc tìm cách thực hiện nhiều tính toán hơn trong cùng một khoảng thời gian

Trước tiên, bạn sẽ xem xét các chương trình liên kết với I/O. Sau đó, bạn sẽ thấy một số mã xử lý các chương trình gắn với CPU

Loại bỏ các quảng cáo

Cách tăng tốc chương trình I/O-Bound

Hãy bắt đầu bằng cách tập trung vào các chương trình liên kết với I/O và một vấn đề phổ biến. tải nội dung qua mạng. Ví dụ của chúng tôi, bạn sẽ tải xuống các trang web từ một số trang web, nhưng nó thực sự có thể là bất kỳ lưu lượng truy cập mạng nào. Nó chỉ dễ dàng hơn để hình dung và thiết lập với các trang web

Phiên bản đồng bộ

Chúng tôi sẽ bắt đầu với phiên bản không đồng thời của tác vụ này. Lưu ý rằng chương trình này yêu cầu mô-đun

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
73. Bạn nên chạy
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
74 trước khi chạy nó, có thể sử dụng virtualenv. Phiên bản này hoàn toàn không sử dụng đồng thời

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9

Như bạn có thể thấy, đây là một chương trình khá ngắn.

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
75 chỉ cần tải xuống nội dung từ một URL và in kích thước. Một điều nhỏ cần chỉ ra là chúng ta đang sử dụng một đối tượng
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
76 từ
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
73

Có thể chỉ cần sử dụng trực tiếp

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
78 từ
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
73, nhưng việc tạo một đối tượng
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
76 cho phép
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
73 thực hiện một số thủ thuật kết nối mạng lạ mắt và thực sự tăng tốc mọi thứ

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
72 tạo
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
76 rồi duyệt qua danh sách các trang web, tải xuống lần lượt từng trang. Cuối cùng, nó in ra quá trình này mất bao lâu để bạn có thể hài lòng khi thấy mức độ đồng thời đã giúp chúng tôi trong các ví dụ sau

Sơ đồ xử lý cho chương trình này sẽ giống như sơ đồ giới hạn I/O trong phần trước

Ghi chú. Lưu lượng mạng phụ thuộc vào nhiều yếu tố có thể thay đổi từ giây này sang giây khác. Tôi đã thấy thời gian của các bài kiểm tra này tăng gấp đôi từ lần chạy này sang lần chạy khác do sự cố mạng

Tại sao phiên bản đồng bộ lại thành công

Điều tuyệt vời về phiên bản mã này là, thật dễ dàng. Nó tương đối dễ viết và gỡ lỗi. Nó cũng đơn giản hơn để suy nghĩ về. Chỉ có một dòng suy nghĩ chạy qua nó, vì vậy bạn có thể dự đoán bước tiếp theo là gì và nó sẽ hoạt động như thế nào

Các vấn đề với phiên bản đồng bộ

Vấn đề lớn ở đây là nó tương đối chậm so với các giải pháp khác mà chúng tôi sẽ cung cấp. Đây là một ví dụ về kết quả cuối cùng trên máy của tôi

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds

Ghi chú. Kết quả của bạn có thể thay đổi đáng kể. Khi chạy tập lệnh này, tôi thấy thời gian thay đổi từ 14. 2 đến 21. 9 giây. Đối với bài viết này, tôi đã chạy nhanh nhất trong ba lần chạy. Sự khác biệt giữa các phương pháp vẫn sẽ rõ ràng

Tuy nhiên, chậm hơn không phải lúc nào cũng là một vấn đề lớn. Nếu chương trình bạn đang chạy chỉ mất 2 giây với phiên bản đồng bộ và hiếm khi chạy, thì có lẽ không đáng để thêm đồng thời. Bạn có thể dừng lại ở đây

Nếu chương trình của bạn được chạy thường xuyên thì sao?

Phiên bản $ ./io_non_concurrent.py [most output skipped] Downloaded 160 in 14.289619207382202 seconds 7

Như bạn có thể đoán, viết một chương trình theo luồng cần nhiều nỗ lực hơn. Tuy nhiên, bạn có thể ngạc nhiên khi thấy cần thêm ít nỗ lực như thế nào đối với các trường hợp đơn giản. Đây là chương trình tương tự trông như thế nào với

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7

import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")

Khi bạn thêm

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7, cấu trúc tổng thể vẫn như cũ và bạn chỉ cần thực hiện một vài thay đổi.
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
72 đã thay đổi từ gọi hàm một lần trên mỗi trang thành cấu trúc phức tạp hơn

Trong phiên bản này, bạn đang tạo một

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
79, điều này có vẻ phức tạp. Hãy phá vỡ điều đó. ________ 279 = ________ 381 + ________ 382 + ________ 383

Bạn đã biết về phần

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
81. Đó chỉ là một dòng suy nghĩ mà chúng tôi đã đề cập trước đó. Phần
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
82 là nơi nó bắt đầu trở nên thú vị. Đối tượng này sẽ tạo một nhóm các luồng, mỗi luồng có thể chạy đồng thời. Cuối cùng,
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
83 là phần sẽ kiểm soát cách thức và thời điểm mỗi luồng trong nhóm sẽ chạy. Nó sẽ thực hiện yêu cầu trong nhóm

Thật hữu ích, thư viện tiêu chuẩn triển khai

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
79 làm trình quản lý bối cảnh để bạn có thể sử dụng cú pháp
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
88 để quản lý việc tạo và giải phóng nhóm
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
89

Khi bạn có một

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
79, bạn có thể sử dụng phương pháp
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
81 tiện dụng của nó. Phương pháp này chạy chức năng được truyền vào trên mỗi trang web trong danh sách. Phần tuyệt vời là nó tự động chạy chúng đồng thời bằng cách sử dụng nhóm luồng mà nó đang quản lý

Những người trong số các bạn đến từ các ngôn ngữ khác, hoặc thậm chí Python 2, có lẽ đang tự hỏi đâu là các đối tượng và hàm thông thường quản lý các chi tiết mà bạn đã quen khi xử lý

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7, những thứ như
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
83,
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
84 và
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
85

Tất cả những thứ này vẫn còn ở đó và bạn có thể sử dụng chúng để đạt được sự kiểm soát chi tiết về cách chạy các chuỗi của bạn. Nhưng, bắt đầu với Python 3. 2, thư viện tiêu chuẩn đã thêm một trừu tượng cấp cao hơn có tên là

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
86 để quản lý nhiều chi tiết cho bạn nếu bạn không cần điều khiển chi tiết đó

Một thay đổi thú vị khác trong ví dụ của chúng ta là mỗi luồng cần tạo đối tượng

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
87 của riêng mình. Khi bạn đang xem tài liệu về
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
73, không nhất thiết phải dễ dàng nhận ra, nhưng khi đọc vấn đề này, có vẻ khá rõ ràng rằng bạn cần một Phiên riêng biệt cho mỗi chủ đề

Đây là một trong những vấn đề thú vị và khó khăn với

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7. Bởi vì hệ điều hành kiểm soát thời điểm tác vụ của bạn bị gián đoạn và tác vụ khác bắt đầu, mọi dữ liệu được chia sẻ giữa các luồng cần được bảo vệ hoặc an toàn cho luồng. Thật không may,
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
87 không an toàn cho luồng

Có một số chiến lược để làm cho truy cập dữ liệu an toàn theo luồng tùy thuộc vào dữ liệu là gì và cách bạn sử dụng dữ liệu đó. Một trong số đó là sử dụng các cấu trúc dữ liệu an toàn theo luồng như

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
85 từ mô-đun
import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
92 của Python

Các đối tượng này sử dụng các nguyên hàm cấp thấp như

import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
93 để đảm bảo rằng chỉ một luồng có thể truy cập một khối mã hoặc một chút bộ nhớ cùng một lúc. Bạn đang sử dụng chiến lược này một cách gián tiếp thông qua đối tượng
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
79

Một chiến lược khác để sử dụng ở đây là thứ gọi là lưu trữ cục bộ luồng.

import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
95 tạo một đối tượng trông giống như toàn cầu nhưng dành riêng cho từng luồng riêng lẻ. Trong ví dụ của bạn, điều này được thực hiện với
import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
96 và
import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
97

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7

import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
98 nằm trong mô-đun
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7 để giải quyết cụ thể vấn đề này. Có vẻ hơi lạ, nhưng bạn chỉ muốn tạo một trong những đối tượng này, không phải một đối tượng cho mỗi luồng. Bản thân đối tượng đảm nhiệm việc tách các truy cập từ các luồng khác nhau sang các dữ liệu khác nhau

Khi

import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
97 được gọi, thì
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
91 mà nó tra cứu là dành riêng cho luồng cụ thể mà nó đang chạy trên đó. Vì vậy, mỗi luồng sẽ tạo một phiên duy nhất trong lần đầu tiên nó gọi
import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
97 và sau đó sẽ chỉ sử dụng phiên đó cho mỗi lần gọi tiếp theo trong suốt vòng đời của nó

Cuối cùng, một lưu ý nhanh về việc chọn số lượng chủ đề. Bạn có thể thấy rằng mã ví dụ sử dụng 5 chủ đề. Vui lòng thử với con số này và xem thời gian tổng thể thay đổi như thế nào. Bạn có thể mong đợi rằng có một luồng cho mỗi lần tải xuống sẽ là nhanh nhất, nhưng ít nhất trên hệ thống của tôi thì không phải vậy. Tôi đã tìm thấy kết quả nhanh nhất ở đâu đó trong khoảng từ 5 đến 10 chủ đề. Nếu bạn tăng cao hơn mức đó, thì chi phí bổ sung cho việc tạo và hủy chuỗi sẽ xóa bất kỳ khoản tiết kiệm thời gian nào

Câu trả lời khó ở đây là số luồng chính xác không phải là hằng số từ nhiệm vụ này sang nhiệm vụ khác. Một số thử nghiệm là bắt buộc

Tại sao phiên bản

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7 lại thành công

nó nhanh. Đây là lần chạy thử nghiệm nhanh nhất của tôi. Hãy nhớ rằng phiên bản không đồng thời mất hơn 14 giây

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7

Đây là sơ đồ thời gian thực hiện của nó trông như thế nào

Python có hỗ trợ xử lý song song không?

Nó sử dụng nhiều luồng để có nhiều yêu cầu mở đến các trang web cùng một lúc, cho phép chương trình của bạn chồng chéo thời gian chờ đợi và nhận được kết quả cuối cùng nhanh hơn. dippee. đó là mục tiêu

Các vấn đề với Phiên bản

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7

Chà, như bạn có thể thấy từ ví dụ, cần thêm một chút mã để thực hiện điều này và bạn thực sự phải suy nghĩ về dữ liệu nào được chia sẻ giữa các chuỗi

Các luồng có thể tương tác theo những cách tinh vi và khó phát hiện. Những tương tác này có thể gây ra các điều kiện chạy đua thường dẫn đến các lỗi ngẫu nhiên, không liên tục và có thể khá khó tìm. Những bạn chưa quen với khái niệm điều kiện cuộc đua có thể muốn mở rộng và đọc phần bên dưới

Điều kiện cuộc đuaHiển thị/Ẩn

Điều kiện chủng tộc là toàn bộ các loại lỗi tinh vi có thể và thường xuyên xảy ra trong mã đa luồng. Điều kiện cạnh tranh xảy ra do lập trình viên không có quyền truy cập dữ liệu được bảo vệ đầy đủ để ngăn chặn các luồng can thiệp lẫn nhau. Bạn cần thực hiện thêm các bước khi viết mã theo luồng để đảm bảo mọi thứ an toàn theo luồng

Điều đang xảy ra ở đây là hệ điều hành đang kiểm soát khi nào luồng của bạn chạy và khi nào nó được hoán đổi để cho một luồng khác chạy. Việc hoán đổi chuỗi này có thể xảy ra tại bất kỳ thời điểm nào, ngay cả khi thực hiện các bước phụ của câu lệnh Python. Như một ví dụ nhanh, nhìn vào chức năng này

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
8

Đoạn mã này khá giống với cấu trúc bạn đã sử dụng trong ví dụ về

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7 ở trên. Sự khác biệt là mỗi luồng đang truy cập cùng một biến toàn cục
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
96 và tăng dần nó.
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
97 không được bảo vệ theo bất kỳ cách nào, vì vậy nó không an toàn cho luồng

Để tăng

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
96, mỗi luồng cần đọc giá trị hiện tại, thêm một giá trị vào giá trị đó và lưu giá trị đó trở lại biến. Điều đó xảy ra trong dòng này.
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
99

Bởi vì hệ điều hành không biết gì về mã của bạn và có thể hoán đổi các luồng tại bất kỳ thời điểm nào trong quá trình thực thi, nên việc hoán đổi này có thể xảy ra sau khi một luồng đã đọc giá trị nhưng trước khi nó có cơ hội ghi lại giá trị đó. Nếu mã mới đang chạy cũng sửa đổi

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
96, thì luồng đầu tiên có một bản sao cũ của dữ liệu và sự cố sẽ xảy ra

Như bạn có thể tưởng tượng, gặp phải tình huống chính xác này là khá hiếm. Bạn có thể chạy chương trình này hàng ngàn lần và không bao giờ gặp sự cố. Đó là điều làm cho loại sự cố này khá khó gỡ lỗi vì nó có thể khá khó tái tạo và có thể khiến các lỗi trông ngẫu nhiên xuất hiện

Ví dụ khác, tôi muốn nhắc bạn rằng

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
87 không an toàn cho luồng. Điều này có nghĩa là có những nơi mà loại tương tác được mô tả ở trên có thể xảy ra nếu nhiều luồng sử dụng cùng một
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
76. Tôi đưa ra điều này không phải để phủ nhận
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
73 mà là để chỉ ra rằng đây là những vấn đề khó giải quyết

Loại bỏ các quảng cáo

Phiên bản $ ./io_non_concurrent.py [most output skipped] Downloaded 160 in 14.289619207382202 seconds 6

Trước khi bạn xem xét mã ví dụ của

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6, hãy nói thêm về cách thức hoạt động của
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 Khái niệm cơ bản

Đây sẽ là phiên bản đơn giản hóa của

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6. Có nhiều chi tiết được làm mờ ở đây, nhưng nó vẫn truyền đạt ý tưởng về cách thức hoạt động của nó

Khái niệm chung về

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 là một đối tượng Python duy nhất, được gọi là vòng lặp sự kiện, kiểm soát cách thức và thời điểm mỗi tác vụ được chạy. Vòng lặp sự kiện nhận thức được từng nhiệm vụ và biết nó đang ở trạng thái nào. Trong thực tế, có nhiều trạng thái mà các tác vụ có thể ở trong đó, nhưng bây giờ hãy tưởng tượng một vòng lặp sự kiện đơn giản hóa chỉ có hai trạng thái.

Trạng thái sẵn sàng sẽ chỉ ra rằng một tác vụ có việc phải làm và sẵn sàng để chạy và trạng thái chờ có nghĩa là tác vụ đang đợi một số thứ bên ngoài kết thúc, chẳng hạn như hoạt động mạng

Vòng lặp sự kiện đơn giản hóa của bạn duy trì hai danh sách nhiệm vụ, một danh sách cho mỗi trạng thái này. Nó chọn một trong các tác vụ đã sẵn sàng và khởi động lại để chạy. Nhiệm vụ đó được kiểm soát hoàn toàn cho đến khi nó hợp tác trao lại quyền kiểm soát cho vòng lặp sự kiện

Khi tác vụ đang chạy trao quyền điều khiển trở lại vòng lặp sự kiện, vòng lặp sự kiện sẽ đặt tác vụ đó vào danh sách sẵn sàng hoặc danh sách chờ, sau đó duyệt qua từng tác vụ trong danh sách chờ để xem liệu tác vụ đó đã sẵn sàng cho một thao tác I/O chưa . Nó biết rằng các tác vụ trong danh sách sẵn sàng vẫn sẵn sàng vì nó biết chúng chưa chạy

Sau khi tất cả các tác vụ đã được sắp xếp lại vào đúng danh sách, vòng lặp sự kiện sẽ chọn tác vụ tiếp theo để chạy và quy trình lặp lại. Vòng lặp sự kiện đơn giản hóa của bạn chọn nhiệm vụ đã đợi lâu nhất và chạy nhiệm vụ đó. Quá trình này lặp lại cho đến khi vòng lặp sự kiện kết thúc

Một điểm quan trọng của

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 là các nhiệm vụ không bao giờ từ bỏ quyền kiểm soát mà không cố ý làm như vậy. Họ không bao giờ bị gián đoạn ở giữa một hoạt động. Điều này cho phép chúng tôi chia sẻ tài nguyên dễ dàng hơn một chút trong
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 so với trong
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7. Bạn không phải lo lắng về việc đảm bảo an toàn cho chuỗi mã của mình

Đó là một cái nhìn cấp cao về những gì đang xảy ra với

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6. Nếu bạn muốn biết thêm chi tiết, câu trả lời StackOverflow này cung cấp một số chi tiết tốt nếu bạn muốn tìm hiểu sâu hơn

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
04 và
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
05

Bây giờ hãy nói về hai từ khóa mới đã được thêm vào Python.

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
04 và
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
05. Theo nội dung thảo luận ở trên, bạn có thể xem
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
05 như phép thuật cho phép tác vụ trao quyền điều khiển trở lại vòng lặp sự kiện. Khi mã của bạn chờ một lệnh gọi hàm, đó là tín hiệu cho thấy lệnh gọi đó có thể sẽ mất một khoảng thời gian và tác vụ đó sẽ từ bỏ quyền kiểm soát

Dễ dàng nhất để nghĩ về

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
04 như một lá cờ để Python nói với nó rằng hàm sắp được xác định sử dụng
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
05. Có một số trường hợp điều này không hoàn toàn đúng, chẳng hạn như trình tạo không đồng bộ, nhưng nó đúng trong nhiều trường hợp và cung cấp cho bạn một mô hình đơn giản khi bạn bắt đầu

Một ngoại lệ cho điều này mà bạn sẽ thấy trong đoạn mã tiếp theo là câu lệnh

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
11, câu lệnh này tạo một trình quản lý bối cảnh từ một đối tượng mà bạn thường chờ đợi. Trong khi ngữ nghĩa là một chút khác nhau, ý tưởng là như nhau. để gắn cờ trình quản lý bối cảnh này là thứ có thể bị tráo đổi

Như tôi chắc rằng bạn có thể tưởng tượng, có một số phức tạp trong việc quản lý sự tương tác giữa vòng lặp sự kiện và các tác vụ. Đối với các nhà phát triển bắt đầu với

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6, những chi tiết này không quan trọng, nhưng bạn cần nhớ rằng bất kỳ chức năng nào gọi
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
05 đều cần được đánh dấu bằng
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
04. Nếu không, bạn sẽ gặp lỗi cú pháp

Quay lại mã

Bây giờ bạn đã có hiểu biết cơ bản về

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 là gì, hãy xem qua phiên bản mã ví dụ
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 và tìm hiểu cách thức hoạt động của nó. Lưu ý rằng phiên bản này thêm
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
17. Bạn nên chạy
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
18 trước khi chạy nó

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
8

Phiên bản này phức tạp hơn một chút so với hai phiên bản trước. Nó có cấu trúc tương tự, nhưng có một chút công việc thiết lập các tác vụ hơn là tạo

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
79. Hãy bắt đầu từ đầu ví dụ

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
75

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
75 ở trên cùng gần giống với phiên bản
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7 ngoại trừ từ khóa
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
04 trên dòng định nghĩa hàm và từ khóa
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
11 khi bạn thực sự gọi
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
25. Sau này bạn sẽ thấy tại sao có thể chuyển
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
76 vào đây thay vì sử dụng lưu trữ cục bộ theo luồng

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
72

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
72 là nơi bạn sẽ thấy sự thay đổi lớn nhất so với ví dụ
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7

Bạn có thể chia sẻ phiên trên tất cả các nhiệm vụ, vì vậy, phiên được tạo ở đây dưới dạng trình quản lý bối cảnh. Các tác vụ có thể chia sẻ phiên vì tất cả chúng đều đang chạy trên cùng một luồng. Không có cách nào một nhiệm vụ có thể làm gián đoạn nhiệm vụ khác trong khi phiên ở trạng thái xấu

Bên trong trình quản lý ngữ cảnh đó, nó tạo một danh sách các tác vụ bằng cách sử dụng

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
30, danh sách này cũng đảm nhiệm việc khởi động chúng. Khi tất cả các tác vụ được tạo, chức năng này sử dụng
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
31 để giữ cho bối cảnh phiên hoạt động cho đến khi tất cả các tác vụ hoàn thành

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7 thực hiện điều gì đó tương tự như thế này, nhưng các chi tiết được xử lý thuận tiện trong mã
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
79. Hiện tại không có lớp
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
34

Tuy nhiên, có một thay đổi nhỏ nhưng quan trọng nằm trong các chi tiết ở đây. Hãy nhớ làm thế nào chúng ta nói về số lượng chủ đề để tạo ra?

Một trong những ưu điểm thú vị của

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 là nó có quy mô tốt hơn nhiều so với
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7. Mỗi tác vụ cần ít tài nguyên hơn và ít thời gian hơn để tạo so với một chuỗi, vì vậy việc tạo và chạy nhiều tác vụ hơn sẽ hoạt động tốt. Ví dụ này chỉ tạo một tác vụ riêng cho từng trang web để tải xuống, hoạt động khá tốt

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
38

Cuối cùng, bản chất của

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 có nghĩa là bạn phải khởi động vòng lặp sự kiện và cho nó biết nhiệm vụ nào sẽ chạy. Phần
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
38 ở cuối tệp chứa mã thành
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
41 và sau đó là
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
42. Nếu không có gì khác, họ đã hoàn thành xuất sắc việc đặt tên cho các chức năng đó

Nếu bạn đã cập nhật lên Python 3. 7, các nhà phát triển lõi Python đã đơn giản hóa cú pháp này cho bạn. Thay vì

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
43 líu lưỡi, bạn chỉ có thể sử dụng
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
44

Tại sao phiên bản

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 lại thành công

Nó thực sự nhanh. Trong các thử nghiệm trên máy của tôi, đây là phiên bản mã nhanh nhất với biên độ tốt

import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
9

Biểu đồ thời gian thực hiện trông khá giống với những gì đang xảy ra trong ví dụ về

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7. Chỉ là các yêu cầu I/O đều được thực hiện bởi cùng một luồng

Python có hỗ trợ xử lý song song không?

Việc thiếu một trình bao bọc đẹp mắt như

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
79 khiến mã này phức tạp hơn một chút so với ví dụ
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7. Đây là trường hợp bạn phải làm thêm một chút để có hiệu suất tốt hơn nhiều

Ngoài ra, có một lập luận phổ biến rằng việc phải thêm

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
04 và
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
05 vào các vị trí thích hợp là một điều phức tạp hơn. Ở một mức độ nhỏ, đó là sự thật. Mặt trái của lập luận này là nó buộc bạn phải suy nghĩ về thời điểm một nhiệm vụ nhất định sẽ được hoán đổi, điều này có thể giúp bạn tạo ra một thiết kế tốt hơn, nhanh hơn.

Vấn đề mở rộng quy mô cũng xuất hiện lớn ở đây. Chạy ví dụ

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7 ở trên với một luồng cho mỗi trang web chậm hơn đáng kể so với chạy nó với một số luồng. Chạy ví dụ về
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 với hàng trăm tác vụ không làm nó chậm lại chút nào

Các vấn đề với phiên bản

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6

Có một số vấn đề với

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 vào thời điểm này. Bạn cần các phiên bản thư viện không đồng bộ đặc biệt để tận dụng tối đa lợi thế của
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6. Nếu bạn vừa sử dụng
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
73 để tải xuống các trang web, nó sẽ chậm hơn nhiều vì
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
73 không được thiết kế để thông báo cho vòng lặp sự kiện rằng nó bị chặn. Vấn đề này ngày càng nhỏ hơn khi thời gian trôi qua và ngày càng có nhiều thư viện chấp nhận
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6

Một vấn đề khác, tế nhị hơn, là tất cả các lợi thế của đa nhiệm hợp tác sẽ bị loại bỏ nếu một trong các nhiệm vụ không hợp tác. Một lỗi nhỏ trong mã có thể khiến một tác vụ bị tắt và giữ bộ xử lý trong một thời gian dài, làm chết các tác vụ khác cần chạy. Không có cách nào để vòng lặp sự kiện đột nhập nếu một tác vụ không trao lại quyền kiểm soát cho nó

Với ý nghĩ đó, chúng ta hãy tiến tới một cách tiếp cận hoàn toàn khác với đồng thời,

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9

Loại bỏ các quảng cáo

Phiên bản $ ./io_non_concurrent.py [most output skipped] Downloaded 160 in 14.289619207382202 seconds 9

Không giống như các phương pháp trước đây, phiên bản mã

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9 tận dụng tối đa nhiều CPU mà máy tính mới, tuyệt vời của bạn có. Hoặc, trong trường hợp của tôi, chiếc máy tính xách tay cũ kỹ, cồng kềnh của tôi có. Hãy bắt đầu với mã

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9

Điều này ngắn hơn nhiều so với ví dụ

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 và thực sự trông khá giống với ví dụ
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7, nhưng trước khi chúng ta đi sâu vào mã, hãy xem nhanh những gì
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9 làm cho bạn

Tóm tắt về

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9

Cho đến thời điểm này, tất cả các ví dụ về đồng thời trong bài viết này chỉ chạy trên một CPU hoặc lõi trong máy tính của bạn. Lý do cho điều này liên quan đến thiết kế hiện tại của CPython và một thứ gọi là Khóa thông dịch viên toàn cầu hoặc GIL

Bài viết này sẽ không đi sâu vào cách thức và lý do của GIL. Bây giờ đủ để biết rằng các phiên bản đồng bộ,

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7 và
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 của ví dụ này đều chạy trên một CPU

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9 trong thư viện tiêu chuẩn được thiết kế để phá vỡ rào cản đó và chạy mã của bạn trên nhiều CPU. Ở cấp độ cao, nó thực hiện điều này bằng cách tạo một phiên bản mới của trình thông dịch Python để chạy trên mỗi CPU và sau đó khai thác một phần chương trình của bạn để chạy trên đó

Như bạn có thể tưởng tượng, việc khởi chạy một trình thông dịch Python riêng không nhanh bằng việc bắt đầu một luồng mới trong trình thông dịch Python hiện tại. Đây là một hoạt động nặng nhọc và đi kèm với một số hạn chế và khó khăn, nhưng đối với vấn đề chính xác, nó có thể tạo ra sự khác biệt lớn

Mã số

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9

Mã này có một số thay đổi nhỏ so với phiên bản đồng bộ của chúng tôi. Cái đầu tiên là trong

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
72. Thay vì chỉ đơn giản gọi
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
75 lặp đi lặp lại, nó tạo ra một đối tượng
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
72 và ánh xạ
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
73 tới đối tượng có thể lặp lại là
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
74. Điều này sẽ trông quen thuộc từ ví dụ
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7

Điều xảy ra ở đây là

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
82 tạo ra một số quy trình thông dịch viên Python riêng biệt và mỗi quy trình chạy chức năng được chỉ định trên một số mục trong iterable, trong trường hợp của chúng tôi là danh sách các trang web. Giao tiếp giữa quy trình chính và các quy trình khác được xử lý bởi mô-đun
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9 cho bạn

Dòng tạo ra

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
82 đáng để bạn quan tâm. Trước hết, nó không chỉ định có bao nhiêu quy trình được tạo trong
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
82, mặc dù đó là một tham số tùy chọn. Theo mặc định,
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
80 sẽ xác định số lượng CPU trong máy tính của bạn và khớp với số lượng đó. Đây thường là câu trả lời hay nhất, và đó là trong trường hợp của chúng tôi

Đối với vấn đề này, việc tăng số lượng quy trình không giúp mọi thứ nhanh hơn. Nó thực sự làm mọi thứ chậm lại vì chi phí thiết lập và phá bỏ tất cả các quy trình đó lớn hơn lợi ích của việc thực hiện các yêu cầu I/O song song

Tiếp theo chúng ta có phần

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
81 của cuộc gọi đó. Hãy nhớ rằng mỗi quy trình trong
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
82 của chúng tôi có không gian bộ nhớ riêng. Điều đó có nghĩa là họ không thể chia sẻ những thứ như đối tượng
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
76. Bạn không muốn tạo một
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
76 mới mỗi khi hàm được gọi, bạn muốn tạo một cái cho mỗi quy trình

Tham số chức năng

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
85 được xây dựng cho trường hợp này. Không có cách nào để chuyển một giá trị trả về từ
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
85 về hàm được gọi bởi quy trình
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
75, nhưng bạn có thể khởi tạo một biến toàn cầu
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
91 để giữ phiên duy nhất cho mỗi quy trình. Bởi vì mỗi tiến trình có không gian bộ nhớ riêng, toàn cục cho mỗi tiến trình sẽ khác nhau

Đó thực sự là tất cả để có nó. Phần còn lại của mã khá giống với những gì bạn đã thấy trước đây

Tại sao phiên bản

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9 lại thành công

Phiên bản

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9 của ví dụ này rất tuyệt vì nó tương đối dễ cài đặt và yêu cầu ít mã bổ sung. Nó cũng tận dụng được hết sức mạnh của CPU trong máy tính của bạn. Sơ đồ thời gian thực hiện cho mã này trông như thế này

Python có hỗ trợ xử lý song song không?

Các vấn đề với phiên bản

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9

Phiên bản ví dụ này yêu cầu một số thiết lập bổ sung và đối tượng toàn cầu

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
91 là lạ. Bạn phải dành thời gian suy nghĩ về những biến nào sẽ được truy cập trong mỗi quy trình

Cuối cùng, nó rõ ràng là chậm hơn phiên bản

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 và
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7 trong ví dụ này

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
88

Điều đó không có gì đáng ngạc nhiên, vì các vấn đề liên quan đến I/O không thực sự là lý do tại sao

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9 tồn tại. Bạn sẽ thấy nhiều hơn khi bước sang phần tiếp theo và xem xét các ví dụ về CPU

Loại bỏ các quảng cáo

Cách tăng tốc chương trình giới hạn CPU

Hãy sang số ở đây một chút. Các ví dụ cho đến nay đều đã xử lý một vấn đề liên quan đến I/O. Bây giờ, bạn sẽ xem xét vấn đề liên quan đến CPU. Như bạn đã thấy, một sự cố liên quan đến I/O dành phần lớn thời gian của nó để chờ các hoạt động bên ngoài, chẳng hạn như một cuộc gọi mạng, hoàn tất. Mặt khác, một sự cố liên quan đến CPU thực hiện ít thao tác I/O và thời gian thực hiện tổng thể của nó là một yếu tố quyết định nó có thể xử lý dữ liệu cần thiết nhanh như thế nào

Với mục đích của ví dụ của chúng tôi, chúng tôi sẽ sử dụng một chức năng hơi ngớ ngẩn để tạo ra thứ gì đó mất nhiều thời gian để chạy trên CPU. Hàm này tính tổng bình phương của mỗi số từ 0 đến giá trị được truyền vào

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
0

Bạn sẽ vượt qua với số lượng lớn, vì vậy sẽ mất một lúc. Hãy nhớ rằng, đây chỉ là một trình giữ chỗ cho mã của bạn thực sự làm điều gì đó hữu ích và đòi hỏi thời gian xử lý đáng kể, chẳng hạn như tính toán nghiệm của các phương trình hoặc sắp xếp một cấu trúc dữ liệu lớn

Phiên bản đồng bộ giới hạn CPU

Bây giờ hãy xem phiên bản không đồng thời của ví dụ

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
1

Mã này gọi

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
96 20 lần với một số lớn khác nhau mỗi lần. Nó thực hiện tất cả những điều này trên một luồng trong một quy trình trên một CPU. Sơ đồ thời gian thực hiện trông như thế này

Python có hỗ trợ xử lý song song không?

Không giống như các ví dụ giới hạn I/O, các ví dụ giới hạn CPU thường khá nhất quán trong thời gian chạy của chúng. Cái này mất khoảng 7. 8 giây trên máy của tôi

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
2

Rõ ràng chúng ta có thể làm tốt hơn thế này. Đây là tất cả chạy trên một CPU không có đồng thời. Hãy xem những gì chúng ta có thể làm để làm cho nó tốt hơn

Phiên bản $ ./io_non_concurrent.py [most output skipped] Downloaded 160 in 14.289619207382202 seconds 7 và $ ./io_non_concurrent.py [most output skipped] Downloaded 160 in 14.289619207382202 seconds 6

Bạn nghĩ việc viết lại mã này bằng cách sử dụng

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7 hoặc
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 sẽ tăng tốc độ này lên bao nhiêu?

Nếu bạn trả lời “Hoàn toàn không”, hãy cho mình một chiếc bánh quy. Nếu bạn trả lời, "Nó sẽ làm nó chậm lại", hãy cho mình hai chiếc bánh quy

Đây là lý do tại sao. Trong ví dụ về giới hạn I/O của bạn ở trên, phần lớn thời gian tổng thể được sử dụng để chờ các thao tác chậm kết thúc.

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7 và
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 đã đẩy nhanh quá trình này bằng cách cho phép bạn chồng chéo thời gian chờ đợi thay vì thực hiện chúng một cách tuần tự

Tuy nhiên, đối với sự cố liên quan đến CPU, không có sự chờ đợi. CPU đang quay nhanh nhất có thể để giải quyết vấn đề. Trong Python, cả luồng và tác vụ đều chạy trên cùng một CPU trong cùng một quy trình. Điều đó có nghĩa là một CPU đang thực hiện tất cả công việc của mã không đồng thời cộng với công việc bổ sung là thiết lập các luồng hoặc tác vụ. Phải mất hơn 10 giây

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
3

Tôi đã viết phiên bản

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7 của mã này và đặt nó cùng với mã ví dụ khác trong repo GitHub để bạn có thể tự kiểm tra mã này. Tuy nhiên, chúng ta đừng nhìn vào điều đó

Loại bỏ các quảng cáo

Phiên bản $ ./io_non_concurrent.py [most output skipped] Downloaded 160 in 14.289619207382202 seconds 9 giới hạn CPU

Bây giờ bạn cuối cùng đã đạt đến nơi mà

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9 thực sự tỏa sáng. Không giống như các thư viện đồng thời khác,
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9 được thiết kế rõ ràng để chia sẻ khối lượng công việc nặng của CPU trên nhiều CPU. Đây là sơ đồ thời gian thực hiện của nó trông như thế nào

Python có hỗ trợ xử lý song song không?

Đây là mã trông như thế nào

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
4

Một chút mã này phải thay đổi từ phiên bản không đồng thời. Bạn phải

import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
07 và sau đó chỉ cần thay đổi từ việc lặp qua các số sang tạo một đối tượng
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
72 và sử dụng phương thức
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
81 của nó để gửi các số riêng lẻ đến các quy trình công nhân khi chúng trở nên miễn phí

Đây chỉ là những gì bạn đã làm cho mã I/O-bound

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9, nhưng ở đây bạn không cần phải lo lắng về đối tượng
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
76

Như đã đề cập ở trên, tham số tùy chọn

import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
12 cho hàm tạo
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
80 đáng được chú ý. Bạn có thể chỉ định có bao nhiêu đối tượng
import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
14 mà bạn muốn tạo và quản lý trong
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
82. Theo mặc định, nó sẽ xác định có bao nhiêu CPU trong máy của bạn và tạo quy trình cho từng CPU. Mặc dù điều này hoạt động tốt cho ví dụ đơn giản của chúng tôi, nhưng bạn có thể muốn kiểm soát nhiều hơn một chút trong môi trường sản xuất

Ngoài ra, như chúng tôi đã đề cập trong phần đầu tiên về

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7, mã
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
72 được xây dựng dựa trên các khối xây dựng như
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
85 và
import concurrent.futures
import requests
import threading
import time


thread_local = threading.local()


def get_session():
    if not hasattr(thread_local, "session"):
        thread_local.session = requests.Session()
    return thread_local.session


def download_site(url):
    session = get_session()
    with session.get(url) as response:
        print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


if __name__ == "__main__":
    sites = [
        "https://www.jython.org",
        "http://olympus.realpython.org/dice",
    ] * 80
    start_time = time.time()
    download_all_sites(sites)
    duration = time.time() - start_time
    print(f"Downloaded {len(sites)} in {duration} seconds")
19 sẽ quen thuộc với những bạn đã thực hiện mã đa luồng và đa xử lý bằng các ngôn ngữ khác

Tại sao phiên bản

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9 lại thành công

Phiên bản

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9 của ví dụ này rất tuyệt vì nó tương đối dễ cài đặt và yêu cầu ít mã bổ sung. Nó cũng tận dụng được hết sức mạnh của CPU trong máy tính của bạn

Này, đó chính xác là những gì tôi đã nói vào lần cuối cùng chúng ta xem xét

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9. Sự khác biệt lớn là lần này nó rõ ràng là lựa chọn tốt nhất. phải mất 2. 5 giây trên máy của tôi

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
5

Điều đó tốt hơn nhiều so với những gì chúng ta đã thấy với các tùy chọn khác

Các vấn đề với phiên bản

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9

Có một số nhược điểm khi sử dụng

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9. Chúng không thực sự xuất hiện trong ví dụ đơn giản này, nhưng việc chia nhỏ vấn đề của bạn để mỗi bộ xử lý có thể hoạt động độc lập đôi khi có thể khó khăn

Ngoài ra, nhiều giải pháp yêu cầu giao tiếp nhiều hơn giữa các quy trình. Điều này có thể thêm một số phức tạp vào giải pháp của bạn mà một chương trình không đồng thời sẽ không cần phải xử lý

Khi nào nên sử dụng đồng thời

Bạn đã hiểu rất nhiều điều ở đây, vì vậy hãy xem lại một số ý tưởng chính và sau đó thảo luận về một số điểm quyết định sẽ giúp bạn xác định mô-đun tương tranh nào, nếu có, bạn muốn sử dụng trong dự án của mình

Bước đầu tiên của quy trình này là quyết định xem bạn có nên sử dụng mô-đun tương tranh hay không. Mặc dù các ví dụ ở đây làm cho mỗi thư viện trông khá đơn giản, nhưng đồng thời luôn đi kèm với độ phức tạp cao hơn và thường có thể dẫn đến các lỗi khó tìm

Tiếp tục thêm đồng thời cho đến khi bạn gặp vấn đề về hiệu suất đã biết và sau đó xác định loại đồng thời nào bạn cần. Như Donald Knuth đã nói, “Tối ưu hóa sớm là gốc rễ của mọi tội lỗi (hoặc ít nhất là phần lớn) trong lập trình. ”

Sau khi bạn đã quyết định rằng mình nên tối ưu hóa chương trình của mình, thì bước tiếp theo là tìm hiểu xem chương trình của bạn có giới hạn CPU hay giới hạn I/O hay không. Hãy nhớ rằng các chương trình liên kết với I/O là những chương trình dành phần lớn thời gian để chờ đợi điều gì đó xảy ra trong khi các chương trình liên kết với CPU dành thời gian để xử lý dữ liệu hoặc xử lý số nhanh nhất có thể

Như bạn đã thấy, các sự cố liên quan đến CPU chỉ thực sự đạt được khi sử dụng

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
9.
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7 và
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 không giúp được loại vấn đề này chút nào

Đối với các vấn đề liên quan đến I/O, có một quy tắc chung trong cộng đồng Python. “Sử dụng

$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 khi bạn có thể,
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
7 khi bạn phải. ”
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6 có thể cung cấp khả năng tăng tốc tốt nhất cho loại chương trình này, nhưng đôi khi bạn sẽ yêu cầu các thư viện quan trọng chưa được chuyển để tận dụng lợi thế của
$ ./io_non_concurrent.py
   [most output skipped]
Downloaded 160 in 14.289619207382202 seconds
6. Hãy nhớ rằng bất kỳ tác vụ nào không từ bỏ quyền kiểm soát vòng lặp sự kiện sẽ chặn tất cả các tác vụ khác

Loại bỏ các quảng cáo

Sự kết luận

Bây giờ bạn đã thấy các loại đồng thời cơ bản có sẵn trong Python

  • $ ./io_non_concurrent.py
       [most output skipped]
    Downloaded 160 in 14.289619207382202 seconds
    
    7
  • $ ./io_non_concurrent.py
       [most output skipped]
    Downloaded 160 in 14.289619207382202 seconds
    
    6
  • $ ./io_non_concurrent.py
       [most output skipped]
    Downloaded 160 in 14.289619207382202 seconds
    
    9

Bạn có hiểu biết để quyết định nên sử dụng phương pháp đồng thời nào cho một vấn đề nhất định hoặc liệu bạn có nên sử dụng bất kỳ phương pháp nào không. Ngoài ra, bạn đã hiểu rõ hơn về một số vấn đề có thể phát sinh khi bạn sử dụng đồng thời

Tôi hy vọng bạn đã học được nhiều điều từ bài viết này và bạn tìm thấy cách sử dụng đồng thời tuyệt vời trong các dự án của riêng mình. Hãy chắc chắn làm bài kiểm tra “Đồng thời Python” của chúng tôi được liên kết bên dưới để kiểm tra việc học của bạn

Lấy bài kiểm tra. Kiểm tra kiến ​​thức của bạn với bài kiểm tra tương tác “Python Concurrency” của chúng tôi. Sau khi hoàn thành, bạn sẽ nhận được điểm số để có thể theo dõi quá trình học tập của mình theo thời gian

Lấy bài kiểm tra "

Đánh dấu là đã hoàn thành

Xem ngay Hướng dẫn này có một khóa học video liên quan do nhóm Real Python tạo. Xem nó cùng với hướng dẫn bằng văn bản để hiểu sâu hơn. Tăng tốc Python bằng đồng thời

🐍 Thủ thuật Python 💌

Nhận một Thủ thuật Python ngắn và hấp dẫn được gửi đến hộp thư đến của bạn vài ngày một lần. Không có thư rác bao giờ. Hủy đăng ký bất cứ lúc nào. Được quản lý bởi nhóm Real Python

Python có hỗ trợ xử lý song song không?

Gửi cho tôi thủ thuật Python »

Về Jim Anderson

Python có hỗ trợ xử lý song song không?
Python có hỗ trợ xử lý song song không?

Jim đã lập trình trong một thời gian dài bằng nhiều ngôn ngữ. Anh ấy đã làm việc trên các hệ thống nhúng, xây dựng các hệ thống xây dựng phân tán, quản lý nhà cung cấp nước ngoài và tham gia rất nhiều cuộc họp

» Thông tin thêm về Jim


Mỗi hướng dẫn tại Real Python được tạo bởi một nhóm các nhà phát triển để nó đáp ứng các tiêu chuẩn chất lượng cao của chúng tôi. Các thành viên trong nhóm đã làm việc trong hướng dẫn này là

Python có hỗ trợ xử lý song song không?

Aldren

Python có hỗ trợ xử lý song song không?

Brad

Python có hỗ trợ xử lý song song không?

David

Python có hỗ trợ xử lý song song không?

Joanna

Bậc thầy Kỹ năng Python trong thế giới thực Với quyền truy cập không giới hạn vào Python thực

Python có hỗ trợ xử lý song song không?

Tham gia với chúng tôi và có quyền truy cập vào hàng nghìn hướng dẫn, khóa học video thực hành và cộng đồng các Pythonistas chuyên gia

Nâng cao kỹ năng Python của bạn »

Bậc thầy Kỹ năng Python trong thế giới thực
Với quyền truy cập không giới hạn vào Python thực

Tham gia với chúng tôi và có quyền truy cập vào hàng ngàn hướng dẫn, khóa học video thực hành và cộng đồng Pythonistas chuyên gia

Nâng cao kỹ năng Python của bạn »

Bạn nghĩ sao?

Đánh giá bài viết này

Tweet Chia sẻ Chia sẻ Email

Bài học số 1 hoặc điều yêu thích mà bạn đã học được là gì?

Mẹo bình luận. Những nhận xét hữu ích nhất là những nhận xét được viết với mục đích học hỏi hoặc giúp đỡ các sinh viên khác. Nhận các mẹo để đặt câu hỏi hay và nhận câu trả lời cho các câu hỏi phổ biến trong cổng thông tin hỗ trợ của chúng tôi

Python có tốt cho đa xử lý không?

Đa xử lý Python dễ thả vào hơn phân luồng nhưng có chi phí bộ nhớ cao hơn . Nếu mã của bạn bị ràng buộc bởi CPU, đa xử lý rất có thể sẽ là lựa chọn tốt hơn—đặc biệt nếu máy mục tiêu có nhiều lõi hoặc CPU.

Python có thực sự đa luồng không?

Python không hỗ trợ đa luồng vì Python trên trình thông dịch Cpython không hỗ trợ thực thi đa lõi thực sự thông qua đa luồng. Tuy nhiên, Python không có thư viện luồng. GIL không ngăn luồng.

Python xử lý đồng thời như thế nào?

Nhiều khi các quy trình đồng thời cần truy cập cùng một dữ liệu vào cùng một thời điểm. Một giải pháp khác, ngoài việc sử dụng khóa rõ ràng, là sử dụng cấu trúc dữ liệu hỗ trợ truy cập đồng thời . Ví dụ: chúng ta có thể sử dụng mô-đun hàng đợi, cung cấp hàng đợi an toàn cho luồng. Chúng ta cũng có thể sử dụng đa xử lý.