How to combine python asyncio with threads?
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.
Introduction
asyncio and threads solve different problems, and sometimes a real Python program needs both. asyncio is great for cooperative asynchronous I/O inside one event loop. Threads are useful when you must integrate blocking code or interact with systems that already run work outside the loop.
The Simplest Direction: Async Code Calling Blocking Code
If you already have an async application and one operation still blocks, the cleanest bridge is usually asyncio.to_thread. It runs a regular synchronous function in a worker thread so the event loop can keep making progress.
This is a good fit for legacy libraries, blocking file operations, or small CPU-light tasks that should not freeze the loop.
Use an Executor When You Need More Control
asyncio.to_thread is convenient, but run_in_executor is still useful when you want to manage the pool explicitly.
Reach for this pattern when thread count, pool reuse, or shutdown behavior matters.
The Reverse Direction: A Thread Submitting Async Work
Sometimes the direction is reversed. You already have a background thread and that thread needs to ask the event loop to run a coroutine. In that case, use asyncio.run_coroutine_threadsafe.
This matters because most event-loop operations are not thread-safe. A foreign thread should not poke arbitrary loop internals directly.
Keep Clear Ownership of the Event Loop
A safe mental model is:
- one event loop lives in one thread
- coroutines run on that loop's thread
- blocking work may run in worker threads
- threads cross into the loop only through thread-safe APIs
You can create an event loop in a dedicated thread, but that should be a deliberate integration choice rather than the default. Most applications are simpler when the main thread owns the loop.
Threads Do Not Make CPU-Bound Python Fast
Threads are useful for responsiveness, but they are not a universal answer for CPU-heavy pure Python work. The global interpreter lock means CPU-bound bytecode does not scale the same way I/O-bound work does.
Threads are still fine when:
- the task mostly waits on external I/O
- the blocking library releases the global interpreter lock
- the goal is to keep the event loop responsive
If the workload is truly CPU-heavy, a process pool may be the better tool.
Avoid Common Integration Mistakes
A lot of problems come from mixing the models carelessly. The classic bug is calling time.sleep inside a coroutine, which blocks the whole event-loop thread. Another is calling asyncio.run from code that is already inside a running loop, creating loop ownership confusion instead of solving the real integration problem.
When you mix threads and async code, shared mutable state also needs normal thread-safety rules. asyncio does not make threaded data races disappear.
Common Pitfalls
- Calling blocking functions directly inside coroutines instead of using
to_threador an executor. - Assuming event-loop methods are thread-safe when they are not.
- Using
asyncio.runrepeatedly instead of having one clear top-level loop owner. - Expecting threads to solve CPU-bound pure Python performance problems.
- Sharing mutable state between threads and coroutines without synchronization.
Summary
- Use
asyncio.to_threadto run blocking synchronous functions without freezing the event loop. - Use
run_in_executorwhen you need more control over the thread pool. - Use
run_coroutine_threadsafewhen another thread must submit work to the loop. - Keep one clear owner for each event loop and cross thread boundaries only through safe APIs.
- For CPU-heavy pure Python work, consider processes instead of threads.
Related reading
- How to configure a fine tuned thread pool for futures?
- How to consume WinRT IAsyncOperation object in native c environment
- How to convert a function in a third party library to be async?
- How to convert a Future into a Stream?
- How to comment out a block of code in Python
- How to comment out a block of Python code in Vim
- How to convert a list of generic tasks of different types that are stored in a ListTask, to a TaskListobject?
- How to convert Dictionarystring, Taskint to TaskDictionarystring, int
.png&w=3840&q=75)
Tackling System Design Interview Problems
A short course that equips you with the skills to approach system design interviews methodically.
Start the free courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.