Reference#

Overview#

To use this library, it is recommended that you instantiate a QtAsyncRunner at the start of the application, using it in context-manager, and then pass that instance along to your main window/widgets:

def main(argv: list[str]) -> None:
    app = QApplication(argv)
    with QtAsyncRunner() as runner:
        main_window = MainWindow(runner)
        main_window.show()
        app.exec()

Using it as a context manager ensures a clean exit.

After that, your MainWindow can pass the runner along to the other parts of the application that need it.

Alternatively, you might decide to create a local QtAsyncRunner instance into a widget (and this is a great way to try the library actually), and it will work fine, but for large scale usage creating a single instance is recommended, to limit the number of running threads in the application.

Classes#

class qt_async_threads.AbstractAsyncRunner#

Abstract interface to a runner.

A runner allow us to start an async function so that the async function (coroutine) can easily submit computational expensive functions to run in a thread, yielding back to the caller so the caller can go and do other things.

This is analogous to what is possible in async libraries like asyncio and trio, but with the difference that this can easily be used from within a normal/blocking function in a Qt application.

This allows us to write slots as async functions that can yield back to the Qt event loop when we want to run something in a thread, and resume the slot once the function finishes (using the concrete QtAsyncRunner implementation).

For tests, there is the SequentialRunner which evaluates the async function to completion in the spot, not executing anything in threads.

Use it as a context manager to ensure proper cleanup.

Usage

Usually you want to connect normal Qt signals to slots written as async functions to signals, something like this:

def _build_ui(self):
    button.clicked.connect(self._on_button_clicked_sync_slot)


def _on_button_clicked_sync_slot(self):
    self.runner.start_coroutine(self._on_button_clicked_async())


async def _on_button_clicked_async(self):
    result = await self.runner.run(compute_spectrum, self.spectrum)

However, the to_sync method can be used to reduce the boilerplate:

def _build_ui(self):
    button.clicked.connect(self.runner.to_sync(self._on_button_clicked))


async def _on_button_clicked(self):
    result = await self.runner.run(compute_spectrum, self.spectrum)

Running many functions in parallel

Often we want to submit several functions to run at the same time (respecting the underlying number of threads in the pool of course), in which case one can use run_parallel:

def compute(*args):
    ...


async def _compute(self) -> None:
    funcs = [partial(compute, ...) for _ in range(attempts)]
    async for result in self.runner.run_parallel(funcs):
        # do something with ``result``
        ...

Using async for, we submit the functions to a thread pool, and we asynchronously process the results as they are completed.

abstract is_idle()#

Return True if this runner is not currently executing anything.

Return type:

bool

abstract close()#

Close runner and cleanup resources.

Cancels all running callables that were started with run or run_parallel. Any callable will still run, however its results will be dropped:

Letting coroutines resume into the main thread after close() has been called can be problematic specially in tests, as close() is often called at the end of the test. If the user has forgotten to properly wait on a coroutine() during the test, often what will happen is that the coroutine will resume (after the thread finishes) when other resources have already been cleared, specially widgets.

Dropping seems harsh, but follows what other libraries like asyncio do when faced with the same situation.

Return type:

None

abstract async run(func, *args, **kwargs)#

Async function which executes the given callable in a separate thread, and yields the control back to async runner while the thread is executing.

Parameters:
  • func (Callable[[~Params], T]) –

  • args (~Params) –

  • kwargs (~Params) –

Return type:

T

abstract async run_parallel(funcs)#

Runs functions in parallel (without arguments, use partial as necessary), yielding their results as they get ready.

Parameters:

funcs (Iterable[Callable[[], T]]) –

Return type:

AsyncIterator[T]

abstract start_coroutine(async_func)#

Starts a coroutine, and returns immediately (except in dummy implementations).

Parameters:

async_func (Coroutine) –

Return type:

None

abstract run_coroutine(coroutine)#

Starts a coroutine, and blocks execution until it finishes, returning its result.

Note: this blocks the call and should be avoided in production, being used as a last resort in cases the main application window or event processing has not started yet (before QApplication.exec()), or for testing.

Parameters:

coroutine (Coroutine[Any, Any, T]) –

Return type:

T

to_sync(async_func)#

Returns a new sync function that will start its coroutine using start_coroutine when called, returning immediately.

Use to connect Qt signals to async functions, see AbstractAsyncRunner docs for example usage.

Parameters:

async_func (Callable[[...], Coroutine[Any, Any, None]]) –

Return type:

Callable[[…], None]

class qt_async_threads.QtAsyncRunner(max_threads=None)#

An implementation of AbstractRunner which runs computational intensive functions using a thread pool.

Parameters:

max_threads (int | None) –

property max_threads: int#

Return the maximum number of threads used by the internal threading pool.

is_idle()#

Return True if this runner is not currently executing anything.

Return type:

bool

close()#

Close runner and cleanup resources.

Cancels all running callables that were started with run or run_parallel. Any callable will still run, however its results will be dropped:

Letting coroutines resume into the main thread after close() has been called can be problematic specially in tests, as close() is often called at the end of the test. If the user has forgotten to properly wait on a coroutine() during the test, often what will happen is that the coroutine will resume (after the thread finishes) when other resources have already been cleared, specially widgets.

Dropping seems harsh, but follows what other libraries like asyncio do when faced with the same situation.

Return type:

None

async run(func, *args, **kwargs)#

Runs the given function in a thread, and while it is running, yields the control back to the Qt event loop.

When the thread finishes, this async function resumes, returning the return value from the function.

Parameters:
  • func (Callable[[~Params], T]) –

  • args (~Params) –

  • kwargs (~Params) –

Return type:

T

async run_parallel(funcs)#

Runs functions in parallel (without arguments, use partial as necessary), yielding their results as they get ready.

Parameters:

funcs (Iterable[Callable[[], T]]) –

Return type:

AsyncIterator[T]

start_coroutine(coroutine)#

Starts the coroutine, and returns immediately.

Parameters:

coroutine (Coroutine) –

Return type:

None

run_coroutine(coroutine)#

Starts the coroutine, doing a busy loop while waiting for it to complete, returning then the result.

Note: see warning in AbstractAsyncRunner about when to use this function.

Parameters:

coroutine (Coroutine[Any, Any, T]) –

Return type:

T

class qt_async_threads.SequentialRunner#

Implementation of an AbstractRunner which doesn’t actually run anything in other threads, acting just as a placeholder in situations we don’t care to run functions in other threads, such as in tests.

is_idle()#

Return True if this runner is not currently executing anything.

Return type:

bool

close()#

Close runner and cleanup resources.

Cancels all running callables that were started with run or run_parallel. Any callable will still run, however its results will be dropped:

Letting coroutines resume into the main thread after close() has been called can be problematic specially in tests, as close() is often called at the end of the test. If the user has forgotten to properly wait on a coroutine() during the test, often what will happen is that the coroutine will resume (after the thread finishes) when other resources have already been cleared, specially widgets.

Dropping seems harsh, but follows what other libraries like asyncio do when faced with the same situation.

Return type:

None

async run(func, *args, **kwargs)#

Sequential implementation, does not really run in a thread, just calls the function and returns its result.

Parameters:
  • func (Callable[[~Params], T]) –

  • args (~Params) –

  • kwargs (~Params) –

Return type:

T

async run_parallel(funcs)#

Sequential implementation, runs functions sequentially in the main thread.

Parameters:

funcs (Iterable[Callable[[], T]]) –

Return type:

AsyncIterator[T]

start_coroutine(coroutine)#

Sequential implementation, just runs the coroutine to completion.

Parameters:

coroutine (Coroutine) –

Return type:

None

run_coroutine(coroutine)#

Runs the given coroutine until it completes, returning its result.

Parameters:

coroutine (Coroutine[None, Any, T]) –

Return type:

T