1. Is it still single threaded when using python async/await?

Yes!

  • When an async function e.g named task1 runs and hits the await keyword, the event loop pauses the execution of task1 and delegates the work to:
    • OS Kernel (for I/O)
    • System timers (for sleep)
    • Thread pool (for CPU-bound work, if used explicitly)
  • After that, the event loop continues and pick up another async function in its queue to execute.
  • While the event loop is executing other tasks, if the OS finishes working on the delegated job and gives back the result. The result is then put into a “result queue”.
  • When all the async tasks in the event loop’s queue are executed, the event loop will pick up the results from the results queue, in the order of first come first serve.
  • The event loop will now continue the execution of the task of first result that gets pickup, and continue until it hits another await or finished the execution.
  • Async functions (async def) run in the same thread and do not block the event loop as long as they contain await.
  • Regular (def) functions block the event loop unless explicitly run in a separate thread or process.

2. What kind of tasks would be best to use async/await?

  • Things like I/O, sleep, network requests, file reads.
  • Best practice: Keep async functions fully async and offload blocking operations to threads or processes when necessary.

3. Prevent blocking?

To prevent blocking, use:

  • asyncio.to_thread() → Runs a regular function in a separate thread (good for I/O).
  • asyncio.run_in_executor() with ProcessPoolExecutor → Runs CPU-heavy tasks in a separate process (avoids GIL limitations).