Debug asyncio callbacks from non-async threads GUI or mDNS
Scanned 9/10/2026
Install to Claude Code
npx -y skills add LoopyLuci/Skills --skill cross-thread-async --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Cross Thread Async?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/loopyluci-cross-thread-async)More formats (shields.io, HTML) on the badges page.
---
name: cross-thread-async
description: "Debug asyncio callbacks from non-async threads GUI or mDNS"
---
# Cross-Thread Async Patterns
Handle asyncio callbacks that fire from **non-async threads** — GUI event loops, mDNS service browsers, hardware polling, or thread-pool workers.
## The Problem
When `asyncio.get_event_loop()` is called from a thread that wasn't started by `asyncio.run()`, Python 3.10+ raises:
```
RuntimeError: There is no current event loop in thread 'zeroconf-ServiceBrowser-...'
```
Because `asyncio.run()` installs the loop only on the calling thread. Sub-threads do not inherit it.
## The Fix: Store the Loop Reference
```python
class ServiceWithCallbacks:
def __init__(self):
self._loop = None # Set externally
def set_event_loop(self, loop):
self._loop = loop
def _on_service_state_change(self):
# WRONG — fails in non-async threads:
# asyncio.get_event_loop() → RuntimeError
# RIGHT:
asyncio.run_coroutine_threadsafe(
self._on_device_added(),
self._loop # stored reference
)
```
**Wire at startup:**
```python
loop = asyncio.get_running_loop()
service.set_event_loop(loop)
```
## When This Pattern Applies
| Scenario | Library | Thread Source |
|----------|---------|--------------|
| mDNS discovery | `zeroconf` | ServiceBrowser background thread |
| GUI toolkit | `tkinter`, `PyQt`, `CustomTkinter` | Main thread ≠ async loop thread |
| File watcher | `watchdog` | Observer thread |
| Hardware events | `pyserial`, `bluetooth` | Reader threads |
| Thread pool | `concurrent.futures` | Worker threads |
## Alternative: Dedicated Async Thread
When ALL async work lives in a background thread:
```python
def background_worker():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_forever()
thread = threading.Thread(target=background_worker, daemon=True)
thread.start()
# Now get_event_loop() works inside this thread
```
Use this for pure async-daemon patterns. Don't use it if the main thread also needs async.
## Detecting the Bug Early
```python
# Add a guard at API boundaries
import asyncio
def schedule_coro(coro):
try:
loop = asyncio.get_running_loop()
# We're in an async context — schedule directly
asyncio.ensure_future(coro)
except RuntimeError:
# No running loop — must have stored reference
raise RuntimeError(
"Called from non-async thread without stored loop reference"
)
```
## Source of the Pattern
Discovered while debugging the InstantTransfer project:
- `zeroconf.ServiceBrowser` fires `ServiceStateChange` callbacks from a background thread
- Calling `asyncio.get_event_loop()` there raises `RuntimeError`
- Fix: store `self._loop = asyncio.get_running_loop()` in the async init, use `asyncio.run_coroutine_threadsafe(coro, self._loop)` in callbacks
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!