πŸ”„ Async vs Sync Queues Deep Dive

Understand the fundamental difference and when to use each.

The Core Question

What happens when a queue is empty and you try to get from it?

queue.Queue

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.

asyncio.Queue

item = await q.get()

Suspends only this coroutine. The event loop stays free to run other coroutines. Same thread, but loop is busy.

The difference: queue.Queue.get() blocks a thread. await asyncio.Queue.get() blocks a coroutine, not the thread.

Visual Timeline: One Thread, Two Jobs

❌ SYNC queue.Queue β€” get() freezes the whole thread

sequenceDiagram participant T as Single Thread participant Q as queue.Queue T->>Q: job1 = q.get() Note over T: πŸ”΄ THREAD FROZEN
job2 cannot even start Q-->>T: item arrives β†’ job1 runs T->>Q: job2 = q.get() Note over T: frozen again until item 2 Q-->>T: item arrives β†’ job2 runs Note over T,Q: TOTAL β‰ˆ 2s (sequential)

βœ… ASYNC asyncio.Queue β€” get() suspends only the coroutine

sequenceDiagram participant L as Event Loop (1 thread) participant Q as asyncio.Queue L->>Q: job1: await q.get() L->>Q: job2: await q.get() Note over L: 🟒 loop FREE β€” both wait concurrently Q-->>L: item β†’ job1 resumes Q-->>L: item β†’ job2 resumes Note over L,Q: TOTAL β‰ˆ 1s (overlapping)

Code Examples

Sync Queue (Multi-Thread)

import 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

Async Queue (Single Thread)

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())

Decision Table: Which Queue?

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.

Thread Safety ⚠️

CRITICAL: asyncio.Queue is NOT thread-safe.

If a thread and a coroutine touch the same asyncio.Queue, you get race conditions and loop corruption.

Solution: Use 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)

Performance Comparison

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)

TL;DR

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.