← Назад к вопросам

Какие у тебя есть вопросы по корутинам?

1.0 Junior🔥 181 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Мои вопросы по корутинам

Как эксперт с 10+ лет опыта, я хотел бы уточнить несколько моментов относительно корутин в Python, которые часто вызывают путаницу даже у опытных разработчиков.

1. Различие между корутинами и asyncio задачами

import asyncio

# Корутина - это объект, результат вызова async функции
async def my_coroutine():
    await asyncio.sleep(1)
    return "Done"

coro = my_coroutine()  # Это корутина
print(type(coro))      # <class coroutine>

# Корутина БЕЗ await просто зависает в памяти
await coro  # Нужно await для исполнения

# Задача (Task) - это корутина, обернутая для запуска в event loop
task = asyncio.create_task(my_coroutine())  # Задача
print(type(task))  # <class asyncio.tasks.Task>

# Задача запланирована и выполнится автоматически
await task

Ключевой момент: Задача (Task) - это корутина плюс информация о статусе выполнения.

2. Почему нельзя просто await корутину дважды?

async def my_coroutine():
    await asyncio.sleep(1)
    return "Done"

coro = my_coroutine()

await coro  # OK
await coro  # RuntimeError: cannot reuse already awaited coroutine

# Правильно:
coro1 = my_coroutine()
coro2 = my_coroutine()

await asyncio.gather(coro1, coro2)

3. Context variables в async коде

from contextvars import ContextVar
import asyncio

user_id = ContextVar("user_id")

async def process_request(uid):
    user_id.set(uid)  # Установить для этой корутины
    await asyncio.sleep(0.1)
    print(f"Processing for user {user_id.get()}")  # Видно значение

async def main():
    # Каждая корутина имеет собственный context
    await asyncio.gather(
        process_request(1),
        process_request(2),
        process_request(3),
    )

asyncio.run(main())
# Output:
# Processing for user 1
# Processing for user 2
# Processing for user 3

Важно: Context variables работают правильно в asyncio (не как thread-local, а как корутина-local).

4. Когда использовать Task vs gather vs wait?

import asyncio

async def task1():
    await asyncio.sleep(1)
    return "task1"

async def task2():
    await asyncio.sleep(2)
    return "task2"

async def task3():
    await asyncio.sleep(0.5)
    return "task3"

async def main():
    # Вариант 1: create_task - для независимых задач
    t1 = asyncio.create_task(task1())
    t2 = asyncio.create_task(task2())
    # Можно делать другое
    print("Tasks created")
    # Потом дождаться
    results = [await t1, await t2]
    
    # Вариант 2: gather - простой синтаксис
    results = await asyncio.gather(task1(), task2(), task3())
    
    # Вариант 3: wait - для сложного управления
    done, pending = await asyncio.wait(
        [asyncio.create_task(task1()),
         asyncio.create_task(task2()),
         asyncio.create_task(task3())],
        return_when=asyncio.FIRST_COMPLETED
    )
    
    for task in done:
        print(f"Completed: {task.result()}")
    for task in pending:
        print(f"Still pending: {task}")

asyncio.run(main())

5. Обработка исключений в async коде

import asyncio

async def failing_task():
    await asyncio.sleep(0.5)
    raise ValueError("Something went wrong")

async def normal_task():
    await asyncio.sleep(0.1)
    return "Success"

async def main():
    # Способ 1: gather с return_exceptions
    results = await asyncio.gather(
        normal_task(),
        failing_task(),
        return_exceptions=True  # Возвращает исключение вместо выброса
    )
    
    for result in results:
        if isinstance(result, Exception):
            print(f"Error: {result}")
        else:
            print(f"Result: {result}")
    
    # Способ 2: try-except для отдельной задачи
    try:
        await failing_task()
    except ValueError as e:
        print(f"Caught: {e}")

asyncio.run(main())

6. Отмена задач (cancellation)

import asyncio

async def long_task():
    try:
        for i in range(100):
            print(f"Working: {i}")
            await asyncio.sleep(0.1)
    except asyncio.CancelledError:
        print("Task was cancelled!")
        # Очистить ресурсы
        raise  # Обязательно перебросить CancelledError

async def main():
    task = asyncio.create_task(long_task())
    
    await asyncio.sleep(0.5)  # Дать задаче поработать
    
    task.cancel()  # Отменить задачу
    
    try:
        await task
    except asyncio.CancelledError:
        print("Task cancellation handled")

asyncio.run(main())

7. Таймауты с asyncio

import asyncio

async def slow_operation():
    await asyncio.sleep(10)
    return "Done"

async def main():
    # Способ 1: timeout (Python 3.11+)
    try:
        async with asyncio.timeout(2):
            result = await slow_operation()
    except asyncio.TimeoutError:
        print("Operation timed out")
    
    # Способ 2: wait_for (старый способ)
    try:
        result = await asyncio.wait_for(slow_operation(), timeout=2)
    except asyncio.TimeoutError:
        print("Operation timed out")

asyncio.run(main())

8. Async контекстные менеджеры

import asyncio

class AsyncResource:
    async def __aenter__(self):
        print("Acquiring resource")
        await asyncio.sleep(0.5)
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print("Releasing resource")
        await asyncio.sleep(0.5)
        return False
    
    async def do_work(self):
        print("Working with resource")
        await asyncio.sleep(0.5)

async def main():
    async with AsyncResource() as resource:
        await resource.do_work()

asyncio.run(main())

9. Async генераторы и async for

import asyncio

async def async_generator():
    for i in range(5):
        print(f"Generating {i}")
        await asyncio.sleep(0.5)
        yield i

async def main():
    # async for работает с async генераторами
    async for value in async_generator():
        print(f"Received {value}")

asyncio.run(main())

10. Race conditions в asyncio

import asyncio

shared_counter = 0

async def increment():
    global shared_counter
    # ❌ RACE CONDITION: несколько корутин могут прочитать одно значение
    for _ in range(1000):
        temp = shared_counter
        await asyncio.sleep(0)  # Точка переключения контекста
        shared_counter = temp + 1  # Может потеряться обновление

async def main():
    await asyncio.gather(increment(), increment(), increment())
    print(shared_counter)  # Значение < 3000

# ✅ Решение: использовать Lock
shared_counter = 0
lock = asyncio.Lock()

async def increment_safe():
    global shared_counter
    for _ in range(1000):
        async with lock:  # Критическая секция
            shared_counter += 1

async def main_safe():
    await asyncio.gather(
        increment_safe(),
        increment_safe(),
        increment_safe()
    )
    print(shared_counter)  # 3000

asyncio.run(main_safe())

Мои рекомендации как эксперта

  1. Для I/O-bound операций - asyncio идеален, используй его.
  2. Для CPU-bound - asyncio НЕ поможет (нет истинного параллелизма).
  3. Всегда оборачивай корутины в Task если нужна независимая работа.
  4. Обрабатывай CancelledError правильно при отмене.
  5. Используй Lock для общих ресурсов - asyncio НЕ гарантирует безопасность без синхронизации.
  6. Context variables вместо глобальных для хранения данных корутины.
  7. Профилируй - asyncio может быть медленнее на простых операциях из-за overhead.

Понимание этих деталей критично для написания надежного асинхронного кода.

Какие у тебя есть вопросы по корутинам? | PrepBro