Appendix_EventLoop
Appendix_EventLoop
Event Loop
import asyncio
loop = asyncio.get_event_loop()
def normal_fn():
print(f"Here is normal function")
async def async_fn():
print(f"Here is async function")
# loop.run_until_complete(normal_fn) # This will be throw exception
loop.run_until_complete(async_fn()) # This will be throw exception
print(loop)
Best Practice
asyncio.run(my_async_fn())Diagram
┌───────────────────────────────┐
│ Python Program │
└───────────────────────────────┘
│
▼
┌───────────────────────┐
│ Main OS Thread │
└───────────────────────┘
│
┌─────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
Loop A Loop B Loop C
(new_event_loop) (new_event_loop) (new_event_loop)
┌───────┐ ┌───────┐ ┌───────┐
│ Idle │ │ Idle │ │ Idle │
└───────┘ └───────┘ └───────┘
▲
│
│ Only ONE loop can be RUNNING
│ in this thread at a time:
│
▼
┌─────────────────────────────────────────┐
│ Running Event Loop (e.g., A) │
│ - Executes async tasks │
│ - Handles timers / sockets │
│ - Manages await points │
└─────────────────────────────────────────┘
Threads and Even loop
┌────────────────────┐
│ Main Thread │
└────────────────────┘
│
▼
Event Loop A
(running or idle)
┌────────────────────┐
│ Worker Thread 1 │
└────────────────────┘
│
▼
Event Loop B
(running or idle)
Important asyncio rules
Rule 1 — One running event loop per thread Each thread may have one running event loop.
Rule 2 — Event loop is NOT shared between threads If you try to use a loop created in Thread A while in Thread B → RuntimeError.
Rule 3 — Threads communicate via thread-safe asyncio functions
loop.call_soon_threadsafe(callback, arg)Examples:
import threading
import asyncio
def thread_worker():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
async def run():
print("Hello from worker loop in thread:", threading.current_thread().name)
loop.run_until_complete(run())
loop.close()
from datetime import datetime
def thread_run_forever():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
async def _run_forever():
while True:
await asyncio.sleep(1)
print(f"Tick: {datetime.now()}")
loop.create_task(_run_forever())
loop.run_forever()
# Main thread
main_loop = asyncio.new_event_loop()
asyncio.set_event_loop(main_loop)
# Spawn worker thread
t = threading.Thread(target=thread_worker)
t.start()
async def main():
print("Hello from main loop in thread:", threading.current_thread().name)
main_loop.run_until_complete(main())
main_loop.close()
t.join()
# Run thread forever ?
t_run_forever = threading.Thread(target=thread_run_forever)
t_run_forever.start()
t_run_forever.join()
Implement signal stop

import threading
import asyncio
import signal
import time
stop_event = threading.Event() # Event to signal thread stop
def thread_worker():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
async def periodic():
while not stop_event.is_set(): # keep running until stop_event is set
print("Tick from worker thread")
await asyncio.sleep(1)
# Stop loop when stop_event is set
def check_stop():
if stop_event.is_set():
loop.stop()
else:
loop.call_later(0.1, check_stop) # check again after 0.1s
# pass
loop.call_soon(check_stop)
loop.create_task(periodic())
loop.run_forever()
# loop.run_until_complete(periodic())
loop.close()
print("Worker thread stopped.")
# Start worker thread
t = threading.Thread(target=thread_worker, name="worker")
t.start()
# Signal handler (Ctrl+C sends SIGINT; terminals do not send Ctrl+X as a signal)
def signal_handler(sig, frame):
if sig == signal.SIGINT:
print("\nCtrl+C pressed, stopping...")
elif sig == signal.SIGTERM:
print("\nSIGTERM received, stopping...")
stop_event.set()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Keep main thread alive without busy-waiting
try:
# Wait until stop_event is set by a signal
while not stop_event.wait(0.1):
pass
except KeyboardInterrupt:
# Fallback: in case default KeyboardInterrupt is raised
stop_event.set()
t.join()
print("Main thread exiting.")
- I use
call_soon,call_laterto callcheck_stopwhich monitor thestop_signalevent in global scope - In
check_stopfunction, we can continue register a function can be executed in theloop. By logical, it register its self to keep continue checking signal for every0.1seconds - We can have other way to implement without using
call_soon,call_laterby register other task which have a loop to monitorstop_signal
# ... In the `thread_worker`
async def check_stop_task():
while not stop_event.is_set():
await asyncio.sleep(0.1)
loop.stop()
# Then register this task
loop.create_task(check_stop_task())
Event loop in async

while True:
# Phase 1 — RUN immediate callbacks
while loop._ready:
handle = loop._ready.pop(0)
handle._run()
# Phase 2 — WAIT for I/O until next timer
timeout = compute_timeout_from_scheduled()
events = selector.select(timeout)
# Convert I/O events into immediate callbacks
for event in events:
loop._ready.append(event.handle)
# Phase 3 — MOVE expired timers into ready queue
now = loop.time()
while loop._scheduled and loop._scheduled[0].when <= now:
timer = heapq.heappop(loop._scheduled)
loop._ready.append(timer)