Learning outcomes
- Explain two event loops
- Schedule work with root.after
- Use async and await correctly
- Communicate through queues
- Keep Tkinter on its thread
- Shut workers down safely
1. Why interfaces freeze
If Adam moves while a five-second decision request runs inside a Tkinter callback, the same thread cannot repaint the window or process the Pause button. The UI appears frozen because its event loop is occupied.
A world tick, a user event and a slow model request are different types of work. They should progress independently and communicate through explicit messages.
2. Tkinter and asyncio are separate schedulers
Tkinter's main loop
root.mainloop() handles widgets, paint events and callbacks. Treat the Tk thread as the only place that modifies Tkinter or Turtle objects.
asyncio's task loop
asyncio coordinates coroutines that yield while waiting. await does not automatically create a thread, and concurrency is not the same as parallel CPU execution.
Calling root.update() repeatedly introduces re-entrant UI work. Calling asyncio.run() from every button creates and destroys loops. Design one stable boundary instead.
3. First scheduler: root.after()
import tkinter as tk
root = tk.Tk()
label = tk.Label(root, text="Tick: 0")
label.pack(padx=24, pady=24)
tick_count = 0
def tick() -> None:
"""Advance the display and return control immediately."""
global tick_count
tick_count += 1
label.config(text=f"Tick: {tick_count}")
root.after(200, tick)
root.after(200, tick)
root.mainloop()root.after() registers future work and returns. It does not sleep. The actual interval includes the time spent by other callbacks, so it is a scheduler rather than a precision clock.
4. Concurrent waiting with asyncio
import asyncio
async def operation(name: str) -> str:
"""Simulate a three-second I/O operation."""
await asyncio.sleep(3)
return f"{name} completed"
async def main() -> None:
results = await asyncio.gather(operation("A"), operation("B"))
print(results)
asyncio.run(main())The two waits overlap because each coroutine yields control. CPU-heavy work would still block this loop and may require a process or specialised executor.
5. The safe bridge
The worker receives plain data and returns plain data. The UI periodically drains a result queue and decides whether the result is still relevant. A request created for tick 12 may be stale when it returns at tick 20.
6. Three independent rhythms
- Simulation rhythm: when rules and needs advance.
- Render rhythm: when the screen reflects the latest state.
- Deliberation rhythm: when an agent needs an expensive decision.
Do not call the LLM on every visual frame or every tick. Deliberation should have a reason: a new goal, failed action, important observation or expired plan.
7. Guided laboratory
Create the freeze
Use a deliberate sleep once and observe the blocked window.
Build the clock
Replace the loop with a self-scheduling root.after callback.
Test coroutines
Run concurrent console tasks before integrating the UI.
Bridge with queues
Return a fake slow decision without touching a widget from the worker.
Protocol completion
A five-second operation does not stop the window moving or the Pause control responding. Closing the application cancels or completes pending work and terminates the worker cleanly.