Lightning talks

Run in another thread without callback

How block the loop?

Some code

CPU intensive

Sync operation

Run in another thread

Use async lib

IO

CPU

Any "aio-{name}" lib?

Run in another thread

async def some_func():
    time.sleep(100)

Run in another thread

import asyncio


def callback():
    time.sleep(100)

async def some_func():
    await asyncio.get_runing_loop().run_in_executor(None, callback)

Run in another thread with args

import asyncio
import functools


def callback(t=100):
    time.sleep(t)

async def some_func():
    some_name = functools.partial(callback, t=10)
    await asyncio.get_runing_loop().run_in_executor(None, some_name)

Threadpool decorator

import asyncio

def run_in_threadpool(func):
    @functools.wraps(func)
    def wrap(*args, **kwargs):
        def inner():
            return func(*args, **kwargs)

        return asyncio.get_running_loop().run_in_executor(None, inner)

    return wrap

@run_in_threadpool
def callback(t=100):
    time.sleep(t)

async def some_func():
    await callback(10)
@run_in_threadpool
def some_function(args):
    some_values = some_code()
    ...
    ...
    return a, b, c


async def handler(request):
    await something(...)
    ...

    a, b, c = await some_function(...)
    some_other_code(b)
    return a

Return values

Any other variants?

async def http_handler(request):
    await some_func()

    async with threadpool():
        time.sleep(100)
        a = 'look mom no def but run in another thread'

    print(a)
    return 'all ok'

Context manager?

pip install asyncio_extras

import asyncio

import threading
import time
from asyncio_extras import threadpool


async def som_handler():
    main_thread_id = threading.get_ident()

    async with threadpool():
        time.sleep(1)
        thread_id = threading.get_ident()

    assert main_thread_id != thread_id


asyncio.run(som_handler())

How does it works?

class _ThreadSwitcher:
    __slots__ = 'executor', 'exited'

    def __init__(self, executor: Optional[Executor]) -> None:
        self.executor = executor
        self.exited = False

    def __aenter__(self):
        # This is run in the event loop thread
        return self

    ...
    ...
    def __await__(self):
        def exec_when_ready():
            event.wait()
            coro.send(None)

            if not self.exited:
                raise RuntimeError('attempted to "await" in a worker thread')

        if self.exited:
            # This is run in the worker thread
            yield
        else:
            # This is run in the event loop thread
            previous_frame = inspect.currentframe().f_back
            coro = next(obj for obj in gc.get_referrers(previous_frame.f_code)
                        if inspect.iscoroutine(obj) and obj.cr_frame is previous_frame)
            event = Event()
            loop = get_event_loop()
            future = loop.run_in_executor(self.executor, exec_when_ready)
            next(future.__await__())  # Make the future think it's being awaited on
            loop.call_soon(event.set)
            yield future
    ...

    def __aexit__(self, exc_type, exc_val, exc_tb):
        # This is run in the worker thread
        self.exited = True
        return self

    ...
    ...
    def __call__(self, func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                loop = get_event_loop()
            except RuntimeError:
                # Event loop not available -- we're in a worker thread
                return func(*args, **kwargs)
            else:
                callback = partial(func, *args, **kwargs)
                return loop.run_in_executor(self.executor, callback)

        assert not inspect.iscoroutinefunction(func), \
            'Cannot wrap coroutine functions to be run in an executor'
        return wrapper

Performance?

No difference found

Limitations?

async def some_asyncio_handler():
    await save_to_db()

    await some_func()

    return 'ok'

Sync code not affected

Also any flask app just ignore client disconnects

async with threadpool()

Run sync like async code, and we can catch CancellError in any place.

This is very dangerous

try:
    val = some_code()
except Exception:
    print('oh no')
    some_fallback()


This is very dangerous

try:
    val = some_code()
except CancellError:
    raise
except Exception:
    print('oh no')
    some_fallback()


Use only if ...

You fully understood _ThreadSwitcher code

Шпасибо

PyConRu#19

By Denis Kataev

PyConRu#19

  • 908