We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Concurrency with Threads and asyncio step 55 of 56
Find Leaked Tasks and Start Threads Together
Build two test helpers. assert_no_task_leaks runs an async function and
reports the tasks it left running. force_race starts several threads that
wait for each other, then each call the same function.
For a body that awaits one task, runs one in a TaskGroup, and leaves two
tasks named "z-leak" and "a-leak" running:
assert_no_task_leaks(body) # (2, ["a-leak", "z-leak"])
assert_no_task_leaks(body)
This is a plain function that returns tuple[int, list[str]]. Use
with asyncio.Runner() as runner: to run an inner coroutine on a new event
loop, as in the article on calling async code from synchronous code. Inside
the coroutine:
- Take a snapshot with
asyncio.all_tasks(). - Call
body()once and await its result, anint. - Take a second snapshot straight away, with no
awaitin between. Pick the tasks that were not in the first snapshot, and leave outasyncio.current_task(). - Record their names with
task.get_name(), sorted. Keep repeats, so two tasks named"idle"give["idle", "idle"]. - Cancel each of those tasks, then await them all with
asyncio.gather(..., return_exceptions=True). - Return the body's result and the names.
Record the names before you cancel, and add no sleep before the second snapshot. Despite its name, the helper returns the names instead of raising.
force_race(n, fn)
- If
n <= 0, return without starting a thread or callingfn. - Otherwise make one
threading.Barrier(n)andnthreads. Each thread waits at the barrier, then callsfnonce. - Start every thread before you join any, then join them all.
The barrier makes every thread wait until all n have arrived, so none
calls fn early. After that, the scheduler still decides which thread runs
first, as the article explains, so a lost update may or may not happen.
What the driver reports
Keep make_body, its helpers and solve as they are. Each entry in tasks
has a name and a mode:
"awaited": the body starts a task that returns1, awaits it, and adds the result to its total."grouped": the body starts the task in aTaskGroup, and adds its result after the group ends."leaked": the body starts a task that waits forever, and never awaits it. The list_PARKEDkeeps the task alive until your snapshot.
The body handles the "awaited" and "leaked" entries in input order, then
runs the "grouped" ones. Then solve calls force_race(race_threads, bump),
where bump adds one to a counter under a lock.
completed: the body's result, which counts the awaited and grouped tasks.leaked: the sorted names your helper recorded.race_total: the counter. It equalsrace_threadswhen that is positive, and0otherwise.
For one awaited task named "work", two leaked tasks named "idle" and
three threads, the report is:
{"completed": 1, "leaked": ["idle", "idle"], "race_total": 3}
An empty task list with zero threads gives 0, [] and 0. Compute the
report from what your helpers observe, not from the input list.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.