Có khối chờ trong Python không?

Các tác vụ ngốn CPU hoặc các thư viện I/O không đồng bộ có thể chặn vòng lặp sự kiện của chương trình của bạn. Tìm hiểu cách tránh điều này trong Python

Hình ảnh của tác giả

Lập trình không đồng bộ đã trở thành mô hình tiêu chuẩn cho thiết kế API và hầu hết các dịch vụ. Phạm vi cho bộ kỹ năng của nhà khoa học dữ liệu cũng đã phát triển. Ngày nay không đủ để tạo ra các mô hình hoặc hình ảnh tốt; . Nếu bạn chưa xử lý lập trình không đồng bộ trong quá trình triển khai của mình, rất có thể bạn sẽ sớm

Nói rõ hơn, câu chuyện này không phải là một hướng dẫn không đồng bộ khác. Nhưng thay vào đó, một số thông tin chi tiết về những trở ngại phổ biến mà một nhà khoa học dữ liệu có thể gặp phải khi kết nối các công cụ của mình với các khung không đồng bộ. Cụ thể, chặn vòng lặp sự kiện bằng các tác vụ ngốn CPU hoặc thư viện I/O không đồng bộ

Câu chuyện này sẽ khám phá cách vòng lặp sự kiện có thể bị chặn và các tài nguyên chúng tôi có để ngăn chặn điều đó

Có nhiều thư viện tốt trong Python xử lý lập trình không đồng bộ, nhưng Asyncio phải là thư viện được đưa vào làm tiêu chuẩn trong Python; . Do đó, trong câu chuyện này, chúng tôi sẽ tập trung vào Asyncio

Cấu trúc câu chuyện

  • Vòng lặp sự kiện
  • Thiết lập thử nghiệm
  • Gọi chức năng chặn một cách ngây thơ
  • Trình thực thi mặc định của Asyncio
  • đồng thời. hợp đồng tương lai ThreadPool
  • đồng thời. hợp đồng tương lai ProcessPool
  • Điểm chuẩn giới hạn I/O
  • Điểm chuẩn giới hạn CPU
  • Đạo đức của câu chuyện

Vòng lặp sự kiện

Cho dù bạn sử dụng mô-đun Asyncio hay bất kỳ thư viện async nào khác, tất cả chúng đều sử dụng vòng lặp sự kiện bên dưới. Vòng lặp sự kiện là một bộ lập lịch chịu trách nhiệm thực thi tất cả các coroutine (chức năng không đồng bộ) trong suốt vòng đời của chương trình

Mô hình đồng thời này về cơ bản là một while (vòng lặp) duy nhất lấy các coroutine và chạy chúng một cách khéo léo. Khi một coroutine đang thực thi, từ khóa await (_______2) sẽ trả lại quyền kiểm soát cho vòng lặp sự kiện để chạy các coroutine khác. Do đó, trong khi vòng lặp sự kiện chờ phản hồi I/O, hoàn thành trong tương lai hoặc đơn giản là ngủ không đồng bộ, nó có thể chạy các coroutine khác. Vòng lặp sự kiện theo dõi những gì sẽ được trả về cho mỗi coroutine và sẽ trả lại nó cho coroutine tương ứng trong các lần lặp lại trong tương lai của vòng lặp

Bây giờ chúng ta đã biết cách thức hoạt động của vòng lặp sự kiện, hãy nghĩ xem điều gì sẽ xảy ra khi chúng ta chạy các tác vụ sử dụng nhiều CPU trong vòng lặp sự kiện. Đây chính xác là nơi cuộc thảo luận trở nên phù hợp với các nhà khoa học dữ liệu. Nếu chúng ta chạy một công việc liên quan đến CPU trong vòng lặp sự kiện, thì vòng lặp đó sẽ chạy tác vụ cho đến khi hoàn thành, giống như bất kỳ vòng lặp while tuần tự và đơn giản nào bạn từng sử dụng. Đó là một vấn đề lớn trong lập trình không đồng bộ vì tất cả các tác vụ khác sẽ phải đợi cho đến khi công việc ngốn CPU của chúng ta hoàn thành

Có ba quy tắc khi nói đến vòng lặp sự kiện

  • bạn không chặn vòng lặp sự kiện
  • bạn không chặn vòng lặp sự kiện
  • bạn không chặn vòng lặp sự kiện

Thoạt nhìn, việc chặn vòng lặp sự kiện nghe có vẻ không tệ lắm. Nhưng nghĩ về kịch bản này. Bạn chịu trách nhiệm mã hóa một mô-đun sẽ cung cấp phân tích dữ liệu trong một ứng dụng (dịch vụ) lớn hơn hiển thị API. API được viết trong một khung không đồng bộ. Nếu bạn bọc các chức năng liên quan đến CPU của mình trong các coroutine, bạn có thể sẽ làm tê liệt toàn bộ ứng dụng. Tất cả các tác vụ khác, như xử lý ứng dụng khách, sẽ bị dừng cho đến khi tác vụ ngốn CPU hoàn thành

Các phần sau đây sẽ xem xét các cách để chạy các tác vụ chặn vòng lặp sự kiện và nghiên cứu hiệu suất của chúng

Thiết lập thử nghiệm

Chúng tôi bắt đầu thử nghiệm chặn vòng lặp sự kiện với hai chức năng

  • một chức năng thực hiện một nhiệm vụ liên quan đến CPU. phép nhân ma trận bằng NumPy
  • một chức năng ngủ chủ đề, tôi. e. , một số I/O không đồng bộ (ví dụ: thư viện cơ sở dữ liệu không đồng bộ);

Hãy dành thời gian (bên ngoài vòng lặp sự kiện) chức năng giới hạn CPU của chúng ta với ma trận vuông gồm 7.000 phần tử mỗi bên để chúng ta biết điều gì sẽ xảy ra

9. 97 giây ± 2. 07 giây trên mỗi vòng lặp (trung bình ± tiêu chuẩn. nhà phát triển. trong số 7 lần chạy, mỗi lần 1 vòng lặp)

Sau đó, chúng tôi tạo một hàm mô phỏng tác vụ I/O định kỳ. Hàm này chạy một vòng lặp cho durarion_secs và nối dấu thời gian hiện tại vào danh sách (time_log) trước khi nhiệm vụ bắt đầu (asyncio.sleep cho sleep_secs) và sau khi hoàn thành

Nhật ký thời gian của chức năng này sẽ là dữ liệu của chúng tôi để đánh giá xem các quy trình khác có đang chặn vòng lặp sự kiện hay không. Hơn nữa, chúng tôi định dạng nhật ký thời gian để chúng tôi chỉ giữ lại sự khác biệt về thời gian trước khi tác vụ được thực thi và ngay sau đó.

Sử dụng thời gian ngủ là 1 mili giây, đây là giao diện của time_log mà không có chức năng nào khác chạy trong vòng lặp sự kiện

Kích thước trục được đo bằng giây [Hình ảnh của tác giả]

Gọi chức năng chặn một cách ngây thơ

Cách tiếp cận đầu tiên mà chúng ta có thể thực hiện, một cách ngây thơ, để tạo một thư viện không đồng bộ là bọc các chức năng chặn của chúng ta trong một coroutine

Để kiểm tra phương pháp này, chúng tôi gói các chức năng của chúng tôi, giới hạn CPU và chặn luồng (time.sleep), bên trong một vòng lặp thực thi chức năng theo định kỳ và nối thêm vào một time_log

Bây giờ chúng tôi chạy đồng thời tất cả các tác vụ, await2 , các chức năng ngủ nhiều CPU và luồng

Ghi chú. Trong mã này, tôi đã chạy await3 bên ngoài một coroutine vì tôi đang sử dụng sổ ghi chép Jupyter, nhưng quy tắc là chỉ nên sử dụng await bên trong coroutine (await5)

Đây là kết quả của nhật ký thời gian được định dạng

Kích thước trục được đo bằng giây [Hình ảnh của tác giả]

Như chúng ta có thể thấy từ nhật ký thời gian I/O (ô đồ con đầu tiên), vòng lặp sự kiện bị chặn. Chúng tôi mong đợi mức trung bình là một phần nghìn giây và thời gian thực hiện là hơn 5 giây trong hầu hết các lần lặp lại

Hai biểu đồ khác cho thấy các tác vụ khác không thực thi mọi lúc trong quá trình thử nghiệm, thay vào đó cạnh tranh để giành tài nguyên và chặn lẫn nhau

Trình thực thi mặc định của Asyncio

Giải pháp để tránh tắc nghẽn vòng lặp sự kiện là thực thi mã chặn của chúng tôi ở nơi khác. Chúng ta có thể sử dụng các luồng hoặc các quy trình khác để thực hiện việc này. Asyncio có một phương pháp lặp rất tiện lợi, await6. Phương pháp này sử dụng giao diện luồng và đa xử lý await7. Cách mặc định để chạy các chức năng chặn của chúng tôi là như vậy

Đối số đầu tiên của await6 được đặt thành await9 (bộ thực thi mặc định), đối số thứ hai là hàm chúng ta muốn chạy trong bộ thực thi và các đối số tiếp theo là đối số của hàm. Trình thực thi mặc định này là yield0 từ await7 với giá trị mặc định

Bao hàm các hàm của chúng ta, tương tự như phần trước và chạy đồng thời các tác vụ

Kết quả của các bản ghi thời gian được định dạng là

Kích thước trục được đo bằng giây [Hình ảnh của tác giả]

Chúng ta có thể thấy rằng vẫn còn một số trục trặc nhỏ trong vòng lặp sự kiện (biểu đồ đầu tiên), nhưng nhật ký thời gian I/O hiển thị chênh lệch thời gian gần một phần nghìn giây. Theo bản thân các tác vụ chặn, chúng đã thực thi đồng thời

đồng thời. hợp đồng tương lai ThreadPool

Chúng ta có thể tùy chỉnh yield0 từ phần trước bằng cách định nghĩa rõ ràng và chuyển nó sang phương thức await6

Sử dụng yield0 chỉ với một nhân viên, chúng tôi bọc các chức năng chặn của mình để thực thi chúng theo định kỳ và ghi nhật ký thời gian, tương tự như các phần trước

Thực hiện đồng thời các tác vụ và vẽ biểu đồ nhật ký thời gian được định dạng

Kích thước trục được đo bằng giây [Hình ảnh của tác giả]

Chúng tôi thấy kết quả tương tự như kết quả thu được trong phần trước;

Điều chỉnh số lượng luồng trong nhóm luồng theo nhu cầu của bạn. Kiểm tra số lượng tối ưu là gì. Số lượng luồng cao hơn không phải lúc nào cũng tốt hơn đối với một số trường hợp sử dụng vì nó gây ra một số chi phí

Trình thực thi ThreadPool tỏa sáng khi xử lý các thư viện I/O không được viết trong mô hình không đồng bộ. Nhiều thư viện cơ sở dữ liệu trong Python chưa hoạt động với async. Sử dụng chúng trong chương trình không đồng bộ của bạn sẽ chặn vòng lặp sự kiện;

đồng thời. hợp đồng tương lai ProcessPool

Cuối cùng, chúng ta có thể sử dụng một quy trình riêng để chạy mã chặn của mình. Chúng tôi làm điều này bằng cách chuyển một thể hiện của yield6 từ yield7 sang phương thức await6

Chúng tôi tạo lại trình bao bọc định kỳ cho thử nghiệm, hiện đang sử dụng các quy trình riêng biệt

Chạy thử nghiệm và vẽ kết quả

Kích thước trục được đo bằng giây [Hình ảnh của tác giả]

Chúng tôi thấy rằng các trục trặc trong I/O

nhật ký thời gian không đáng kể như trong các trường hợp trước. Các quá trình chặn cũng được thực hiện đồng thời

Đa xử lý có thể là một giải pháp tốt trong một số trường hợp, đặc biệt là đối với các tác vụ liên quan đến CPU (không phải tác vụ ngủ luồng) mất nhiều thời gian hơn. Tạo nhiều quy trình mới và di chuyển dữ liệu xung quanh rất tốn kém. Tôi. e. , hãy chắc chắn rằng bạn sẵn sàng trả giá cho việc đa xử lý

Điểm chuẩn giới hạn I/O

Biểu đồ sau đây cho thấy sự khác biệt về thời gian của nhật ký thời gian (càng ít càng tốt) đối với quy trình đăng ký giả I/O (trước và sau khi nhiệm vụ được hoàn thành) bằng cách sử dụng bốn phương pháp được nêu trong các phần trước

Kích thước trục được đo bằng giây [Hình ảnh của tác giả]

Trong mọi trường hợp, chúng tôi muốn những chênh lệch này gần bằng 1 mili giây vì đó là giá trị lý thuyết. Một số điểm không nhất quán có thể chấp nhận được nhưng không chênh lệch quá 4 giây, như khi chúng tôi chặn vòng lặp sự kiện. Kết quả nhóm luồng (trình thực thi mặc định và ThreadPool với một công nhân) không khác biệt đáng kể. Tuy nhiên, kết quả cho ProcessPool cho thấy rõ ràng rằng trình thực thi này sẽ gây ra sự gián đoạn ít nhất cho vòng lặp sự kiện

Điểm chuẩn giới hạn CPU

Biểu đồ sau đây cho thấy thời gian cần thiết (càng ít càng tốt) để hoàn thành tác vụ liên quan đến CPU đối với tất cả các phương pháp đã thảo luận trước đó

Kích thước trục được đo bằng giây [Hình ảnh của tác giả]

Chúng ta có thể thấy rằng việc gọi hàm chặn của chúng ta một cách ngây thơ sẽ mang lại kết quả tốt nhất. Nó có ý nghĩa; . Đối với ba người thi hành khác, kết quả của họ là tương đương nhau; . Di chuyển dữ liệu từ quy trình chính sang quy trình rẽ nhánh mất một thời gian

Trong bất kỳ trường hợp nào, chúng tôi có thể nói rằng việc sử dụng một trình thực thi sẽ không dẫn đến hiệu suất quá cao và vòng lặp sự kiện sẽ không bị tắc nghẽn đáng kể. Tiến hành kiểm tra hiệu suất của bạn để chọn người thực thi (và cấu hình) phù hợp. Tuy nhiên, bạn không nên bắt đầu với trình thực thi mặc định và sử dụng nó làm điểm chuẩn

Đạo đức của câu chuyện

Cho đến nay, chúng ta đã biết rằng việc chặn vòng lặp sự kiện là điều quan trọng chúng ta phải tránh khi thực hiện lập trình không đồng bộ. Nếu bạn quản lý để giữ cho vòng lặp sự kiện không bị tắc nghẽn, mọi thứ sẽ ổn;

Đang chờ chặn hay không

Bởi vì await chỉ hợp lệ bên trong các hàm và mô-đun không đồng bộ, bản thân chúng không đồng bộ và trả về lời hứa, nên biểu thức await không bao giờ chặn luồng chính . e. bất cứ điều gì sau biểu thức chờ đợi.

Điều gì xảy ra khi bạn gọi await Python?

Từ khóa await chuyển điều khiển chức năng trở lại vòng lặp sự kiện . (Nó tạm dừng việc thực thi coroutine xung quanh. ) Nếu Python gặp một biểu thức await f() trong phạm vi của g() , thì đây là cách await báo cho vòng lặp sự kiện, “Tạm dừng thực thi g() cho đến khi tôi đang đợi bất cứ thứ gì—kết quả của f() —là .

Chờ đợi có chặn dòng mã tiếp theo không?

Hiển thị hoạt động trên bài đăng này. Nói tóm lại, chờ đợi chỉ chặn (các) mã sau câu lệnh chờ đợi trong hàm async hiện tại, không phải toàn bộ luồng. Khi sự chờ đợi đã được giải quyết, phần còn lại của mã sẽ được thực thi

Điều gì xảy ra khi sự chờ đợi được gọi?

Khi quá trình thực thi đạt đến biểu thức chờ, mã được tạo sẽ kiểm tra xem thứ bạn đang chờ đã có chưa . Nếu có, bạn có thể sử dụng nó và tiếp tục. Nếu không, nó sẽ thêm phần tiếp theo vào phần "đang chờ" và quay lại ngay lập tức.