web-dev-qa-db-fra.com

Comment utiliser asyncio avec la bibliothèque de blocage existante?

J'ai peu de fonctions de blocage foo, bar et je ne peux pas les changer (une bibliothèque interne que je ne contrôle pas. Parle à un ou plusieurs services réseau). Comment l'utiliser comme async?. Par exemple. Je ne veux pas faire ce qui suit.

results = []
for inp in inps:
    val = foo(inp)
    result = bar(val)
    results.append(result)

Ce sera inefficace car je peux appeler foo pour la deuxième entrée pendant que j'attends la première et même pour bar. Comment les encapsuler de manière à ce qu'ils soient utilisables avec asyncio (c'est-à-dire la nouvelle syntaxe async, await)?

Supposons que les fonctions sont rentrantes. c'est-à-dire qu'il est bien d'appeler à nouveau foo alors qu'un foo précédent est déjà en cours de traitement.


Mise à jour

Extension de la réponse avec un décorateur réutilisable. Cliquez sur ici par exemple.

def run_in_executor(f):
    @functools.wraps(f)
    def inner(*args, **kwargs):
        loop = asyncio.get_running_loop()
        return loop.run_in_executor(None, functools.partial(f, *args, **kwargs))

    return inner
23
balki

Il y a (en quelque sorte) deux questions ici: premièrement, comment exécuter le code de blocage de manière asynchrone, et deuxièmement, comment exécuter le code asynchrone en parallèle (asyncio est monothread, donc le GIL s'applique toujours, donc il ne l'est pas vraiment simultané, mais je m'écarte).

Les tâches parallèles peuvent être créées en utilisant asyncio.ensure_future, comme documenté ici .

Pour exécuter du code synchrone, vous devrez exécuter le code de blocage dans un exécuteur . Exemple:

import concurrent.futures
import asyncio
import time

def blocking(delay):
    time.sleep(delay)
    print('Completed.')

async def non_blocking(loop, executor):
    # Run three of the blocking tasks concurrently. asyncio.wait will
    # automatically wrap these in Tasks. If you want explicit access
    # to the tasks themselves, use asyncio.ensure_future, or add a
    # "done, pending = asyncio.wait..." assignment
    await asyncio.wait(
        fs={
            # Returns after delay=12 seconds
            loop.run_in_executor(executor, blocking, 12),

            # Returns after delay=14 seconds
            loop.run_in_executor(executor, blocking, 14),

            # Returns after delay=16 seconds
            loop.run_in_executor(executor, blocking, 16)
        },
        return_when=asyncio.ALL_COMPLETED
    )

loop = asyncio.get_event_loop()
executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
loop.run_until_complete(non_blocking(loop, executor))

Si vous souhaitez planifier ces tâches à l'aide d'une boucle for (comme dans votre exemple), vous avez plusieurs stratégies différentes, mais l'approche sous-jacente est de planifier le les tâches utilisant la boucle for (ou compréhension de liste, etc.), attendez-les avec asyncio.wait, et puis récupérez les résultats. Exemple:

done, pending = await asyncio.wait(
    fs=[loop.run_in_executor(executor, blocking_foo, *args) for args in inps],
    return_when=asyncio.ALL_COMPLETED
)

# Note that any errors raise during the above will be raised here; to
# handle errors you will need to call task.exception() and check if it
# is not None before calling task.result()
results = [task.result() for task in done]
20
Nick Badger

Prolonger la réponse acceptée pour réellement résoudre le problème en question.

Remarque: Nécessite python 3.7+

import functools

from urllib.request import urlopen
import asyncio


def legacy_blocking_function():  # You cannot change this function
    r = urlopen("https://example.com")
    return r.read().decode()


def run_in_executor(f):
    @functools.wraps(f)
    def inner(*args, **kwargs):
        loop = asyncio.get_running_loop()
        return loop.run_in_executor(None, lambda: f(*args, **kwargs))

    return inner


@run_in_executor
def foo(arg):  # Your wrapper for async use
    resp = legacy_blocking_function()
    return f"{arg}{len(resp)}"


@run_in_executor
def bar(arg):  # Another wrapper
    resp = legacy_blocking_function()
    return f"{len(resp)}{arg}"


async def process_input(inp):  # Modern async function (coroutine)
    res = await foo(inp)
    res = f"XXX{res}XXX"
    return await bar(res)


async def main():
    inputs = ["one", "two", "three"]
    input_tasks = [asyncio.create_task(process_input(inp)) for inp in inputs]
    print([await t for t in asyncio.as_completed(input_tasks)])
    # This doesn't work as expected :(
    # print([await t for t in asyncio.as_completed([process_input(inp) for inp in input_tasks])])


if __name__ == '__main__':
asyncio.run(main())

Cliquez sur ici pour une version à jour de cet exemple et pour envoyer des demandes de tirage.

8
balki
import asyncio
from time import sleep
import logging

logging.basicConfig(
    level=logging.DEBUG, format="%(asctime)s %(thread)s %(funcName)s %(message)s")


def long_task(t):
    """Simulate long IO bound task."""
    logging.info("2. t: %s", t)
    sleep(t)
    logging.info("4. t: %s", t)
    return t ** 2


async def main():
    loop = asyncio.get_running_loop()
    inputs = range(1, 5)
    logging.info("1.")
    futures = [loop.run_in_executor(None, long_task, i) for i in inputs]
    logging.info("3.")
    results = await asyncio.gather(*futures)
    logging.info("5.")
    for (i, result) in Zip(inputs, results):
        logging.info("6. Result: %s, %s", i, result)


if __name__ == "__main__":
    asyncio.run(main())

Production:

2020-03-18 17:13:07,523 23964 main 1.
2020-03-18 17:13:07,524 5008 long_task 2. t: 1
2020-03-18 17:13:07,525 21232 long_task 2. t: 2
2020-03-18 17:13:07,525 22048 long_task 2. t: 3
2020-03-18 17:13:07,526 25588 long_task 2. t: 4
2020-03-18 17:13:07,526 23964 main 3.
2020-03-18 17:13:08,526 5008 long_task 4. t: 1
2020-03-18 17:13:09,526 21232 long_task 4. t: 2
2020-03-18 17:13:10,527 22048 long_task 4. t: 3
2020-03-18 17:13:11,527 25588 long_task 4. t: 4
2020-03-18 17:13:11,527 23964 main 5.
2020-03-18 17:13:11,528 23964 main 6. Result: 1, 1
2020-03-18 17:13:11,528 23964 main 6. Result: 2, 4
2020-03-18 17:13:11,529 23964 main 6. Result: 3, 9
2020-03-18 17:13:11,529 23964 main 6. Result: 4, 16
0
Vlad Bezden