Skip to content

loop.call_soon_threadsafe() behaves as non-thread-safe in free-threading #720

@x42005e1f

Description

@x42005e1f

When I tried running rogerbinns/python-async-bench on free-threaded Python 3.14.2 (Linux, dual-core), I found it getting stuck on the uvloop (0.22.1) tests. Below is the code to reproduce the issue and its possible output.

#!/usr/bin/env python3

import asyncio
import threading
import time

from queue import SimpleQueue

import uvloop

START_TIME = time.monotonic()


def callback(future):
    future.set_result(True)
    print(f"time: {time.monotonic() - START_TIME:.10f} (callback)")


def notify_all(loop, queue):
    while (future := queue.get()) is not None:
        loop.call_soon_threadsafe(callback, future)


async def main():
    loop = asyncio.get_running_loop()
    queue = SimpleQueue()
    thread = threading.Thread(target=notify_all, args=[loop, queue])
    thread.start()

    try:
        async with asyncio.timeout(60):
            while True:
                future = loop.create_future()

                print(f"time: {time.monotonic() - START_TIME:.10f}")

                try:
                    async with asyncio.timeout(3):
                        outer_future = asyncio.shield(future)
                        queue.put(future)
                        await outer_future
                except TimeoutError:
                    print("[timeout] future:", future)
                    raise
    except TimeoutError:
        pass
    finally:
        queue.put(None)

    await asyncio.to_thread(thread.join)


if __name__ == "__main__":
    uvloop.run(main())
...
time: 0.1763970610
time: 0.1764364019 (callback)
time: 0.1764616738
time: 0.1765011698 (callback)
time: 0.1765267970
time: 3.1786725279 (callback)
[timeout] future: <Future finished result=True>

This looks like a race condition. As we can see, the handle was successfully added, but was only processed by the _on_idle() method after the timeout. It is probably related to the fact that access to the _ready_len attribute is performed non-atomically from different threads (or, in a simpler case, self._ready_len = len(self._ready) from the main thread competes with self._ready_len += 1 from the worker thread). This also applies to loop.run_in_executor() and asyncio.to_thread() running on top of it, but it is much more difficult to reproduce the issue with them (due to the greater delay between threads).

Related: #408.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions