Phát triển với asyncio

Lập trình không đồng bộ khác với lập trình "tuần tự" cổ điển.

Trang này liệt kê các lỗi và bẫy phổ biến cũng như giải thích cách tránh chúng.

Chế độ gỡ lỗi

Theo mặc định, asyncio chạy ở chế độ sản xuất. Để dễ dàng phát triển, asyncio có debug mode.

Có một số cách để bật chế độ gỡ lỗi asyncio:

Ngoài việc bật chế độ gỡ lỗi, hãy xem xét thêm:

  • đặt mức nhật ký của asyncio logger thành logging.DEBUG, ví dụ: đoạn mã sau có thể chạy khi khởi động ứng dụng:

    logging.basicConfig(level=logging.DEBUG)
    
  • định cấu hình mô-đun warnings để hiển thị cảnh báo ResourceWarning. Một cách để làm điều đó là sử dụng tùy chọn dòng lệnh -W default.

Khi chế độ gỡ lỗi được bật:

  • Nhiều API asyncio không an toàn theo luồng (chẳng hạn như các phương thức loop.call_soon()loop.call_at()) đưa ra một ngoại lệ nếu chúng được gọi từ một luồng sai.

  • Thời gian thực hiện của bộ chọn I/O được ghi lại nếu mất quá nhiều thời gian để thực hiện thao tác I/O.

  • Các cuộc gọi lại kéo dài hơn 100 mili giây sẽ được ghi lại. Thuộc tính loop.slow_callback_duration có thể được sử dụng để đặt thời lượng thực hiện tối thiểu tính bằng giây được coi là "chậm".

Đồng thời và đa luồng

Vòng lặp sự kiện chạy trong một luồng (thường là luồng chính) và thực thi tất cả lệnh gọi lại và Nhiệm vụ trong luồng của nó. Trong khi một Tác vụ đang chạy trong vòng lặp sự kiện thì không có Tác vụ nào khác có thể chạy trong cùng một chuỗi. Khi một Tác vụ thực thi biểu thức await, Tác vụ đang chạy sẽ bị tạm dừng và vòng lặp sự kiện sẽ thực thi Tác vụ tiếp theo.

Để lên lịch callback từ một luồng hệ điều hành khác, nên sử dụng phương thức loop.call_soon_threadsafe(). Ví dụ:

loop.call_soon_threadsafe(gọi lại, *args)

Hầu hết tất cả các đối tượng asyncio đều không an toàn cho luồng, điều này thường không thành vấn đề trừ khi có mã hoạt động với chúng từ bên ngoài Tác vụ hoặc lệnh gọi lại. Nếu cần có mã như vậy để gọi asyncio API cấp thấp, thì nên sử dụng phương thức loop.call_soon_threadsafe(), ví dụ:

loop.call_soon_threadsafe(fut.cancel)

Để lên lịch cho một đối tượng coroutine từ một luồng hệ điều hành khác, nên sử dụng hàm run_coroutine_threadsafe(). Nó trả về concurrent.futures.Future để truy cập kết quả:

async def coro_func():
     trở về đang chờ asyncio.sleep(1, 42)

# Later trong một chuỗi hệ điều hành khác:

tương lai = asyncio.run_coroutine_threadsafe(coro_func(), vòng lặp)
# Wait để biết kết quả:
kết quả = tương lai.result()

Để xử lý tín hiệu, vòng lặp sự kiện phải được chạy trong luồng chính.

Phương thức loop.run_in_executor() có thể được sử dụng với concurrent.futures.ThreadPoolExecutor hoặc InterpreterPoolExecutor để thực thi mã chặn trong một luồng hệ điều hành khác mà không chặn luồng hệ điều hành mà vòng lặp sự kiện chạy trong đó.

Hiện tại không có cách nào để lên lịch coroutine hoặc lệnh gọi lại trực tiếp từ một quy trình khác (chẳng hạn như quy trình bắt đầu bằng multiprocessing). Phần Phương pháp vòng lặp sự kiện liệt kê các API có thể đọc từ các đường ống và xem phần mô tả tệp mà không chặn vòng lặp sự kiện. Ngoài ra, API Subprocess của asyncio cung cấp cách bắt đầu một quy trình và liên lạc với nó từ vòng lặp sự kiện. Cuối cùng, phương pháp loop.run_in_executor() nói trên cũng có thể được sử dụng với concurrent.futures.ProcessPoolExecutor để thực thi mã trong một quy trình khác.

Chạy mã chặn

Không nên gọi trực tiếp mã chặn (CPU-bound). Ví dụ: nếu một hàm thực hiện phép tính chuyên sâu CPU trong 1 giây thì tất cả các Tác vụ asyncio và hoạt động IO đồng thời sẽ bị trễ 1 giây.

Một trình thực thi có thể được sử dụng để chạy một tác vụ trong một luồng khác, kể cả trong một trình thông dịch khác hoặc thậm chí trong một quy trình khác để tránh chặn luồng hệ điều hành bằng vòng lặp sự kiện. Xem phương pháp loop.run_in_executor() để biết thêm chi tiết.

Ghi nhật ký

asyncio sử dụng mô-đun logging và tất cả việc ghi nhật ký được thực hiện thông qua bộ ghi "asyncio".

Mức nhật ký mặc định là logging.INFO, có thể dễ dàng điều chỉnh:

logging.getLogger("asyncio").setLevel(logging.WARNING)

Ghi nhật ký mạng có thể chặn vòng lặp sự kiện. Nên sử dụng một luồng riêng để xử lý nhật ký hoặc sử dụng IO không chặn. Ví dụ: xem Xử lý các trình xử lý chặn.

Phát hiện các coroutine không bao giờ được chờ đợi

Khi một hàm coroutine được gọi nhưng không được chờ đợi (ví dụ: coro() thay vì await coro()) hoặc coroutine không được lên lịch với asyncio.create_task(), asyncio sẽ phát ra RuntimeWarning:

nhập asyncio

kiểm tra độ phân giải không đồng bộ():
    print("chưa bao giờ lên lịch")

async def main():
    kiểm tra()

asyncio.run(chính())

Đầu ra:

test.py:7: RuntimeWarning: coroutine 'test' chưa bao giờ được chờ đợi
  kiểm tra()

Đầu ra ở chế độ gỡ lỗi:

test.py:7: RuntimeWarning: coroutine 'test' chưa bao giờ được chờ đợi
Coroutine được tạo vào lúc (cuộc gọi gần đây nhất gần đây nhất)
  Tệp "../t.py", dòng 9, trong <module>
    asyncio.run(main(), debug=True)

  < .. >

  Tệp "../t.py", dòng 7, trong tệp chính
    kiểm tra()
  kiểm tra()

Cách khắc phục thông thường là chờ coroutine hoặc gọi hàm asyncio.create_task()

async def main():
    đang chờ kiểm tra()

Phát hiện các ngoại lệ không bao giờ được truy xuất

Nếu Future.set_exception() được gọi nhưng đối tượng Future không bao giờ được chờ đợi, thì ngoại lệ sẽ không bao giờ được truyền tới mã người dùng. Trong trường hợp này, asyncio sẽ phát ra thông báo tường trình khi đối tượng Tương lai được thu gom rác.

Ví dụ về một ngoại lệ chưa được xử lý:

nhập asyncio

lỗi không đồng bộ lỗi():
    tăng ngoại lệ ("không được tiêu thụ")

async def main():
    asyncio.create_task(bug())

asyncio.run(chính())

Đầu ra:

Ngoại lệ nhiệm vụ không bao giờ được truy xuất
tương lai: <Task đã hoàn thành coro=<bug() đã hoàn thành, được xác định tại test.py:3>
  ngoại lệ=Ngoại lệ('không được tiêu thụ')>

Traceback (cuộc gọi gần đây nhất):
  Tệp "test.py", dòng 4, bị lỗi
    tăng ngoại lệ ("không được tiêu thụ")
Ngoại lệ: không tiêu thụ

Enable the debug mode để lấy lại dấu vết nơi tác vụ được tạo:

asyncio.run(main(), debug=True)

Đầu ra ở chế độ gỡ lỗi:

Ngoại lệ nhiệm vụ không bao giờ được truy xuất
tương lai: <Task đã hoàn thành coro=<bug() đã hoàn thành, được xác định tại test.py:3>
    ngoại lệ=Ngoại lệ('không tiêu thụ') được tạo tại asyncio/tasks.py:321>

source_traceback: Đối tượng được tạo tại (cuộc gọi gần đây nhất gần đây nhất):
  Tệp "../t.py", dòng 9, trong <module>
    asyncio.run(main(), debug=True)

< .. >

Traceback (cuộc gọi gần đây nhất):
  Tệp "../t.py", dòng 4, bị lỗi
    tăng ngoại lệ ("không được tiêu thụ")
Ngoại lệ: không tiêu thụ

Thực tiễn tốt nhất về máy phát điện không đồng bộ

Viết mã asyncio chính xác và hiệu quả đòi hỏi phải nhận thức được những cạm bẫy nhất định. Phần này phác thảo các phương pháp hay nhất cần thiết có thể giúp bạn tiết kiệm hàng giờ gỡ lỗi.

Đóng các trình tạo không đồng bộ một cách rõ ràng

Bạn nên đóng asynchronous generator theo cách thủ công. Nếu trình tạo thoát sớm - ví dụ: do ngoại lệ xuất hiện trong phần nội dung của vòng lặp async for - mã dọn dẹp không đồng bộ của nó có thể chạy trong ngữ cảnh không mong muốn. Điều này có thể xảy ra sau khi các tác vụ mà nó phụ thuộc đã hoàn thành hoặc trong quá trình tắt vòng lặp sự kiện khi móc thu gom rác của trình tạo không đồng bộ được gọi.

Để tránh điều này, hãy đóng trình tạo một cách rõ ràng bằng cách gọi phương thức aclose() của nó hoặc sử dụng trình quản lý bối cảnh contextlib.aclosing()

nhập asyncio
nhập khẩu ngữ cảnh

async def gen():
  năng suất 1
  năng suất 2

async def func():
  không đồng bộ với contextlib.aclosing(gen())  g:
    không đồng bộ cho x trong g:
      break # Don không lặp lại cho đến hết

asyncio.run(func())

Như đã lưu ý ở trên, mã dọn dẹp cho các trình tạo không đồng bộ này bị hoãn lại. Ví dụ sau chứng minh rằng việc hoàn thiện trình tạo không đồng bộ có thể xảy ra theo thứ tự không mong muốn:

nhập asyncio
Work_done = Sai

con trỏ def không đồng bộ():
    thử:
        năng suất 1
    cuối cùng:
        khẳng định công việc_hoàn thành

hàng def không đồng bộ():
    công việc toàn cầu_done
    thử:
        năng suất 2
    cuối cùng:
        đang chờ asyncio.sleep(0.1) # immitate một số công việc không đồng bộ
        công việc_done = Đúng


async def main():
    không đồng bộ cho c trong con trỏ():
        không đồng bộ cho r trong hàng():
            phá vỡ
        phá vỡ

asyncio.run(chính())

Đối với ví dụ này, chúng tôi nhận được kết quả đầu ra sau:

ngoại lệ chưa được xử  trong quá trình tắt asyncio.run()
nhiệm vụ: <Nhiệm vụ đã hoàn thành tên='Task-3' coro=<<async_generator_athrow không  __name__>()> ngoại lệ=AssertionError()>
Traceback (cuộc gọi gần đây nhất):
  Tệp "example.py", dòng 6, trong con trỏ
    năng suất 1
asyncio.Exceptions.CancelledError

Trong quá trình xử  ngoại lệ trên, một ngoại lệ khác đã xảy ra:

Traceback (cuộc gọi gần đây nhất):
  Tệp "example.py", dòng 8, trong con trỏ
    khẳng định công việc_hoàn thành
           ^^ ^^^ ^^ ^^
Khẳng địnhLỗi

Trình tạo không đồng bộ cursor() đã được hoàn thiện trước trình tạo rows - một hành vi không mong muốn.

Ví dụ này có thể được khắc phục bằng cách đóng rõ ràng các trình tạo async-generator cursorrows

async def main():
    không đồng bộ với contextlib.aclosing(cursor()) dưới dạng con trỏ_gen:
        không đồng bộ cho c trong con trỏ_gen:
            không đồng bộ với contextlib.aclosing(rows()) dưới dạng row_gen:
                không đồng bộ cho r trong row_gen:
                    phá vỡ
            phá vỡ

Chỉ tạo các trình tạo không đồng bộ khi vòng lặp sự kiện đang chạy

Bạn chỉ nên tạo asynchronous generators sau khi vòng lặp sự kiện được tạo.

Để đảm bảo rằng các trình tạo không đồng bộ đóng một cách đáng tin cậy, vòng lặp sự kiện sử dụng hàm sys.set_asyncgen_hooks() để đăng ký các hàm gọi lại. Các lệnh gọi lại này cập nhật danh sách các trình tạo không đồng bộ đang chạy để giữ cho nó ở trạng thái nhất quán.

Khi hàm loop.shutdown_asyncgens() được gọi, các bộ tạo đang chạy sẽ dừng nhẹ nhàng và danh sách sẽ bị xóa.

Trình tạo không đồng bộ gọi móc hệ thống tương ứng trong lần lặp đầu tiên của nó. Đồng thời, bộ tạo ghi rằng hook đã được gọi và không gọi lại.

Do đó, nếu quá trình lặp bắt đầu trước khi vòng lặp sự kiện được tạo, vòng lặp sự kiện sẽ không thể thêm trình tạo vào danh sách các trình tạo đang hoạt động của nó vì các hook được đặt sau khi trình tạo cố gắng gọi chúng. Do đó, vòng lặp sự kiện sẽ không thể kết thúc trình tạo nếu cần thiết.

Hãy xem xét ví dụ sau:

nhập asyncio

async def agenfn():
    thử:
        năng suất 10
    cuối cùng:
        đang chờ asyncio.sleep(0)


với asyncio.Runner() làm người chạy:
    agen = agenfn()
    print(runner.run(anext(agen)))
    del tuổi

Đầu ra:

10
Ngoại lệ bị bỏ qua khi đóng trình tạo < đối tượng async_generator agenfn tại 0x000002F71CD10D70>:
Traceback (cuộc gọi gần đây nhất):
  Tệp "example.py", dòng 13, trong <module>
    del tuổi
        ^^ ^^
RuntimeError: trình tạo không đồng bộ bị bỏ qua GeneratorExit

Ví dụ này có thể được sửa như sau:

nhập asyncio

async def agenfn():
    thử:
        năng suất 10
    cuối cùng:
        đang chờ asyncio.sleep(0)

async def main():
    agen = agenfn()
    in(chờ tiếp theo(agen))
    del tuổi

asyncio.run(chính())

Tránh lặp lại và đóng đồng thời của cùng một trình tạo

Trình tạo không đồng bộ có thể được nhập lại trong khi một cuộc gọi __anext__() / athrow() / aclose() khác đang diễn ra. Điều này có thể dẫn đến trạng thái không nhất quán của trình tạo async và có thể gây ra lỗi.

Hãy xem xét ví dụ sau:

nhập asyncio

người tiêu dùng def async():
    cho idx trong phạm vi (100):
        đang chờ asyncio.sleep(0)
        tin nhắn = năng suất idx
        in('đã nhận', tin nhắn)

async def amain():
    máy phát điện = người tiêu dùng()
    đang chờ agenerator.asend(Không )

    fa = asyncio.create_task(agenerator.asend('A'))
    fb = asyncio.create_task(agenerator.asend('B'))
    chờ đợi
    chờ đợi fb

asyncio.run(amain())

Đầu ra:

đã nhận được A
Traceback (cuộc gọi gần đây nhất):
  Tệp "test.py", dòng 38, trong <module>
    asyncio.run(amain())
    ~~~~~~~~~~~~ ^^ ^^ ^^ ^^ ^^
  Tệp "Lib/asyncio/runners.py", dòng 204, đang chạy
    trả về Runner.run(chính)
           ~~~~~~~~~~~~ ^^ ^^ ^^
  Tệp "Lib/asyncio/runners.py", dòng 127, đang chạy
    trả về self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^^ ^^ ^^
  Tệp "Lib/asyncio/base_events.py", dòng 719, trong run_until_complete
    trả về tương lai.result()
           ~~~~~~~~~~~~~~ ^^
  Tệp "test.py", dòng 36, trong amain
    chờ đợi fb
RuntimeError: anext(): trình tạo không đồng bộ đang chạy

Do đó, nên tránh sử dụng trình tạo không đồng bộ trong các tác vụ song song hoặc trên nhiều vòng lặp sự kiện.