Understand the fundamental difference and when to use each.
What happens when a queue is empty and you try to get from it?
item = q.get()
Freezes the ENTIRE thread. The thread blocks at the OS level, waiting for an item. Other threads can still run, but THIS thread is asleep.
item = await q.get()
Suspends only this coroutine. The event loop stays free to run other coroutines. Same thread, but loop is busy.
queue.Queue.get() blocks a thread. await asyncio.Queue.get() blocks a coroutine, not the thread.
queue.Queue β get() freezes the whole threadasyncio.Queue β get() suspends only the coroutineimport queue
import threading
q = queue.Queue()
def job1():
item = q.get() # β blocks THREAD
print(f"job1 got: {item}")
def job2():
item = q.get() # β can't run until job1 wakes up
print(f"job2 got: {item}")
# Both on same thread? Can't!
# They're functions, not coroutines.
# NEED separate threads:
t1 = threading.Thread(target=job1)
t2 = threading.Thread(target=job2)
t1.start()
t2.start()
q.put("for job2") # arrives ~immediately
q.put("for job1") # job1 still waiting, hasn't been put yet
import asyncio
q = asyncio.Queue()
async def job1():
item = await q.get() # β suspends job1 only
print(f"job1 got: {item}")
async def job2():
item = await q.get() # β suspends job2 only
print(f"job2 got: {item}")
async def main():
# Both run on ONE thread, wait concurrently
await asyncio.gather(job1(), job2())
# Put items
await q.put("for job2") # job2 wakes up immediately
await q.put("for job1") # job1 wakes up immediately
asyncio.run(main())
| Your Setup | Use | Why |
|---|---|---|
| Multiple threads, each with its own queue | queue.Queue |
Thread-safe by design. Each thread has its own blocking point. |
| One event loop, many coroutines sharing a queue | asyncio.Queue |
Suspends coroutines, not the thread. Loop stays free to run others. |
| Threads produce, coroutines consume (or vice versa) | janus.Queue |
Thread-safe bridge with both .sync_q and .async_q sides. |
| Simple blocking work on one thread | queue.Queue |
Simpler; you're not doing concurrency anyway. |
asyncio.Queue is NOT thread-safe.
asyncio.Queue, you get race conditions and loop corruption.
janus.Queue for threadβcoroutine bridges.
import janus
# Thread-safe bridge
q = janus.Queue()
# Thread side
def thread_producer():
for i in range(10):
q.sync_q.put(f"item {i}") # thread-safe
# Coroutine side
async def async_consumer():
while True:
item = await q.async_q.get() # async-safe
print(item)
| Metric | queue.Queue + Threads | asyncio.Queue + Coroutines |
|---|---|---|
| Memory per task | ~1-2 MB (thread stack) | ~50-100 KB (coroutine object) |
| Max concurrent on 1 machine | ~10K threads (OS limit) | ~100K+ coroutines (limited by memory only) |
| Context-switch overhead | High (OS scheduler) | Low (coroutine yields explicitly) |
| Use case | CPU-bound (multiprocessing), long-lived parallel workers | I/O-bound (thousands of short-lived tasks) |
queue.Queue: Blocks a thread. Use when you have separate threads.
asyncio.Queue: Suspends a coroutine, loop stays free. Use when you have many coroutines on one event loop.
janus.Queue: Thread-safe bridge between threads and coroutines.
The choice: Are you writing code with threads, or with async/await? That tells you which queue to use.