asynchronous programming
synchronous routines
concurrency
notification methods
programming techniques

How can I notify an async routine from a sync routine?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A synchronous routine cannot await, so it cannot notify an async routine by directly calling async code and expecting a result immediately. The usual solution is to hand work or a signal to the event loop through a thread-safe mechanism such as an event, queue, or scheduled coroutine.

Use the Event Loop as the Handoff Point

In Python asyncio, the event loop owns async tasks. If synchronous code needs to notify an async routine, the safe design is:

  • async code waits on an event or queue
  • sync code signals that event through the loop
  • the loop wakes the async task

Here is a complete example using asyncio.Event:

python
1import asyncio
2import threading
3import time
4
5
6def notify(loop, event):
7    time.sleep(1)
8    loop.call_soon_threadsafe(event.set)
9
10
11async def worker(event):
12    print("waiting for notification")
13    await event.wait()
14    print("async routine notified")
15
16
17async def main():
18    loop = asyncio.get_running_loop()
19    event = asyncio.Event()
20
21    thread = threading.Thread(target=notify, args=(loop, event))
22    thread.start()
23
24    await worker(event)
25    thread.join()
26
27
28asyncio.run(main())

The important call is loop.call_soon_threadsafe(event.set). That schedules the notification safely onto the event loop from synchronous code running in another thread.

Use run_coroutine_threadsafe When the Sync Side Must Trigger Async Work

Sometimes the synchronous routine does not just want to set a flag. It wants to launch a coroutine. In that case, schedule the coroutine onto the loop explicitly:

python
1import asyncio
2import threading
3import time
4
5
6async def do_work(message):
7    print(f"async work: {message}")
8
9
10def notify(loop):
11    time.sleep(1)
12    future = asyncio.run_coroutine_threadsafe(do_work("started from sync code"), loop)
13    future.result()
14
15
16async def main():
17    loop = asyncio.get_running_loop()
18    thread = threading.Thread(target=notify, args=(loop,))
19    thread.start()
20    await asyncio.sleep(2)
21    thread.join()
22
23
24asyncio.run(main())

This is useful when the sync side wants to submit real async work rather than just flip a shared event.

Choose the Right Notification Primitive

The best primitive depends on the shape of the communication:

  • use Event for a simple signal
  • use Queue when data must be passed
  • use run_coroutine_threadsafe when the sync side needs to schedule a coroutine directly

The async routine should wait in an async-friendly way. The sync side should never try to manipulate async objects directly without going through the loop's thread-safe entry points.

Common Pitfalls

The biggest mistake is calling async functions from synchronous code without an event-loop handoff. Creating a coroutine object is not the same as running it.

Another issue is touching asyncio primitives from the wrong thread. If synchronous code runs outside the loop thread, use call_soon_threadsafe or a similar safe bridge.

Developers also sometimes block the event loop while waiting for the sync side to finish. That defeats the purpose of async coordination and can freeze the whole system.

Finally, if the sync side needs a response, design that explicitly. Notification is one-way by default. Request-response usually needs a future, queue, or another callback path.

Summary

  • Sync code cannot notify async code by awaiting directly.
  • Use the event loop as the handoff boundary.
  • 'loop.call_soon_threadsafe is the standard way to signal async primitives from sync code in another thread.'
  • 'asyncio.run_coroutine_threadsafe is useful when sync code must schedule a coroutine.'
  • Pick an Event, Queue, or scheduled coroutine based on whether you need a signal, data, or executable async work.
  • A thread-safe handoff is more reliable than shared mutable flags polled from async code.
  • If sync code needs a result back, pair the notification with a future or queue.
  • Keep the event loop free to process the notification promptly.
  • Direct cross-thread access to asyncio objects is a correctness bug.

Course illustration
Course illustration

All Rights Reserved.