Chamada de método assíncrona em Python?


178

Eu queria saber se existe alguma biblioteca para chamadas de método assíncronas em Python . Seria ótimo se você pudesse fazer algo como

@async
def longComputation():
    <code>


token = longComputation()
token.registerCallback(callback_function)
# alternative, polling
while not token.finished():
    doSomethingElse()
    if token.finished():
        result = token.result()

Ou para chamar uma rotina não assíncrona de forma assíncrona

def longComputation()
    <code>

token = asynccall(longComputation())

Seria ótimo ter uma estratégia mais refinada como nativa no núcleo do idioma. Isso foi considerado?


No Python 3.4: docs.python.org/3/library/asyncio.html (há um backport para 3.3 e uma nova asynce brilhante awaitsintaxe do 3.5).
jonrsharpe

Não há mecanismo de retorno de chamada, mas você pode agregar resultados em um dicionário e ele é baseado no módulo de multiprocessamento do Python. Tenho certeza de que você pode adicionar mais um parâmetro à função decorada como retorno de chamada. github.com/alex-sherman/deco .
RajaRaviVarma

Para começar. Documentação oficial - docs.python.org/3/library/concurrency.html
Adarsh ​​Madrecha

Respostas:


141

Você pode usar o módulo de multiprocessamento adicionado no Python 2.6. Você pode usar conjuntos de processos e obter resultados de forma assíncrona com:

apply_async(func[, args[, kwds[, callback]]])

Por exemplo:

from multiprocessing import Pool

def f(x):
    return x*x

if __name__ == '__main__':
    pool = Pool(processes=1)              # Start a worker processes.
    result = pool.apply_async(f, [10], callback) # Evaluate "f(10)" asynchronously calling callback when finished.

Esta é apenas uma alternativa. Este módulo fornece muitas facilidades para alcançar o que você deseja. Também será muito fácil fazer um decorador com isso.


5
Lucas S., seu exemplo não funciona, infelizmente. A função de retorno de chamada nunca é chamada.
DataGreed

6
Provavelmente vale a pena ter em mente que isso gera processos separados, em vez de threads separados dentro de um processo. Isso pode ter algumas implicações.
user47741

11
Isso funciona: resultado = pool.apply_async (f, [10], retorno de chamada = conclusão)
MJ

6
Realmente fazer algo de forma assíncrona em python requer o uso do módulo de multiprocessamento para gerar novos processos. A simples criação de novos threads ainda está à mercê do Global Interpreter Lock, que impede que um processo python faça várias coisas ao mesmo tempo.
Drahkar

2
Caso você não queira gerar um novo processo ao usar esta solução - altere a importação para from multiprocessing.dummy import Pool. multiprocessing.dummy tem o mesmo comportamento exato implementado ao longo de tópicos em vez de processos
Almog Cohen

203

Algo como:

import threading

thr = threading.Thread(target=foo, args=(), kwargs={})
thr.start() # Will run "foo"
....
thr.is_alive() # Will return whether foo is running currently
....
thr.join() # Will wait till "foo" is done

Consulte a documentação em https://docs.python.org/library/threading.html para obter mais detalhes.


1
Sim, se você só precisa fazer as coisas de forma assíncrona, por que não usar o thread? afinal fio é leve do que o processo
kk1957

22
Nota importante: a implementação padrão (CPython) de threads não ajudará nas tarefas ligadas à computação, devido ao "Bloqueio global de intérpretes". Consulte a biblioteca doc: link
solublefish

3
O uso de thread.join () é realmente assíncrono? E se você não quiser bloquear um thread (por exemplo, um thread da interface do usuário) e não usar uma tonelada de recursos fazendo um loop while?
Mgamerz

1
A junção @Mgamerz é síncrona. Você pode deixar o encadeamento colocar os resultados da execução em alguma fila ou / e chamar um retorno de chamada. Caso contrário, você não sabe quando está pronto (se houver).
Drakosha

1
É possível chamar uma função de chamada de retorno no final da execução do thread como você pode fazer com multiprocessing.Pool
Reda Drissi

49

No Python 3.5, você pode usar geradores aprimorados para funções assíncronas.

import asyncio
import datetime

Sintaxe aprimorada do gerador:

@asyncio.coroutine
def display_date(loop):
    end_time = loop.time() + 5.0
    while True:
        print(datetime.datetime.now())
        if (loop.time() + 1.0) >= end_time:
            break
        yield from asyncio.sleep(1)


loop = asyncio.get_event_loop()
# Blocking call which returns when the display_date() coroutine is done
loop.run_until_complete(display_date(loop))
loop.close()

Nova async/awaitsintaxe:

async def display_date(loop):
    end_time = loop.time() + 5.0
    while True:
        print(datetime.datetime.now())
        if (loop.time() + 1.0) >= end_time:
            break
        await asyncio.sleep(1)


loop = asyncio.get_event_loop()
# Blocking call which returns when the display_date() coroutine is done
loop.run_until_complete(display_date(loop))
loop.close()

8
@carnabeh, você poderia estender esse exemplo para incluir a função "def longComputation ()" do OP? A maioria dos exemplos usa "waitit asyncio.sleep (1)", mas se longComputation () retornar, digamos, um duplo, você não pode simplesmente usar "waitit longComputation ()".
Fab

Dez anos no futuro e essa deve ser a resposta aceita agora. Quando você fala sobre assíncrono em python3.5 +, o que vem à mente deve ser asyncio e async keyword.
zeh 22/06


21

Você pode implementar um decorador para tornar suas funções assíncronas, embora isso seja um pouco complicado. O multiprocessingmódulo está cheio de pequenas peculiaridades e restrições aparentemente arbitrárias - mais um motivo para encapsulá-lo atrás de uma interface amigável.

from inspect import getmodule
from multiprocessing import Pool


def async(decorated):
    r'''Wraps a top-level function around an asynchronous dispatcher.

        when the decorated function is called, a task is submitted to a
        process pool, and a future object is returned, providing access to an
        eventual return value.

        The future object has a blocking get() method to access the task
        result: it will return immediately if the job is already done, or block
        until it completes.

        This decorator won't work on methods, due to limitations in Python's
        pickling machinery (in principle methods could be made pickleable, but
        good luck on that).
    '''
    # Keeps the original function visible from the module global namespace,
    # under a name consistent to its __name__ attribute. This is necessary for
    # the multiprocessing pickling machinery to work properly.
    module = getmodule(decorated)
    decorated.__name__ += '_original'
    setattr(module, decorated.__name__, decorated)

    def send(*args, **opts):
        return async.pool.apply_async(decorated, args, opts)

    return send

O código abaixo ilustra o uso do decorador:

@async
def printsum(uid, values):
    summed = 0
    for value in values:
        summed += value

    print("Worker %i: sum value is %i" % (uid, summed))

    return (uid, summed)


if __name__ == '__main__':
    from random import sample

    # The process pool must be created inside __main__.
    async.pool = Pool(4)

    p = range(0, 1000)
    results = []
    for i in range(4):
        result = printsum(i, sample(p, 100))
        results.append(result)

    for result in results:
        print("Worker %i: sum value is %i" % result.get())

Em um caso do mundo real, eu elaboraria um pouco mais sobre o decorador, fornecendo uma maneira de desativá-lo para depuração (mantendo a interface futura no lugar) ou talvez um recurso para lidar com exceções; mas acho que isso demonstra o princípio suficientemente bem.


Essa deve ser a melhor resposta. Eu amo como ele pode retornar valor. Não é como o thread que simplesmente roda de forma assíncrona.
Aminah Nuraini 24/03

16

Somente

import threading, time

def f():
    print "f started"
    time.sleep(3)
    print "f finished"

threading.Thread(target=f).start()

8

Você poderia usar o eventlet. Ele permite que você escreva o que parece ser um código síncrono, mas que ele opere de forma assíncrona na rede.

Aqui está um exemplo de um rastreador super mínimo:

urls = ["http://www.google.com/intl/en_ALL/images/logo.gif",
     "https://wiki.secondlife.com/w/images/secondlife.jpg",
     "http://us.i1.yimg.com/us.yimg.com/i/ww/beta/y3.gif"]

import eventlet
from eventlet.green import urllib2

def fetch(url):

  return urllib2.urlopen(url).read()

pool = eventlet.GreenPool()

for body in pool.imap(fetch, urls):
  print "got body", len(body)

7

Minha solução é:

import threading

class TimeoutError(RuntimeError):
    pass

class AsyncCall(object):
    def __init__(self, fnc, callback = None):
        self.Callable = fnc
        self.Callback = callback

    def __call__(self, *args, **kwargs):
        self.Thread = threading.Thread(target = self.run, name = self.Callable.__name__, args = args, kwargs = kwargs)
        self.Thread.start()
        return self

    def wait(self, timeout = None):
        self.Thread.join(timeout)
        if self.Thread.isAlive():
            raise TimeoutError()
        else:
            return self.Result

    def run(self, *args, **kwargs):
        self.Result = self.Callable(*args, **kwargs)
        if self.Callback:
            self.Callback(self.Result)

class AsyncMethod(object):
    def __init__(self, fnc, callback=None):
        self.Callable = fnc
        self.Callback = callback

    def __call__(self, *args, **kwargs):
        return AsyncCall(self.Callable, self.Callback)(*args, **kwargs)

def Async(fnc = None, callback = None):
    if fnc == None:
        def AddAsyncCallback(fnc):
            return AsyncMethod(fnc, callback)
        return AddAsyncCallback
    else:
        return AsyncMethod(fnc, callback)

E funciona exatamente como solicitado:

@Async
def fnc():
    pass

5

Algo assim funciona para mim, você pode chamar a função e ela será despachada para um novo thread.

from thread import start_new_thread

def dowork(asynchronous=True):
    if asynchronous:
        args = (False)
        start_new_thread(dowork,args) #Call itself on a new thread.
    else:
        while True:
            #do something...
            time.sleep(60) #sleep for a minute
    return

2

Existe algum motivo para não usar threads? Você pode usar a threadingclasse Em vez da finished()função, use o isAlive(). A result()função poderia join()encadear e recuperar o resultado. E, se puder, substitua as funções run()e __init__para chamar a função especificada no construtor e salve o valor em algum lugar da instância da classe.


2
Se for um encadeamento de funções computacionalmente caro, você não obterá nada (provavelmente tornará as coisas mais lentas, na verdade), pois um processo Python é limitado a um núcleo de CPU devido ao GIL.
22410 Kurt

2
@ Kurt, embora isso seja verdade, o OP não mencionou que o desempenho era sua preocupação. Há outras razões para querer comportamento assíncrono ...
Peter Hansen

Threads em python não são ótimos quando você deseja ter a opção de eliminar a chamada de método assíncrona, pois apenas o thread principal em python recebe sinais.
precisa saber é o seguinte

2

Você pode usar concurrent.futures (adicionado no Python 3.2).

import time
from concurrent.futures import ThreadPoolExecutor


def long_computation(duration):
    for x in range(0, duration):
        print(x)
        time.sleep(1)
    return duration * 2


print('Use polling')
with ThreadPoolExecutor(max_workers=1) as executor:
    future = executor.submit(long_computation, 5)
    while not future.done():
        print('waiting...')
        time.sleep(0.5)

    print(future.result())

print('Use callback')
executor = ThreadPoolExecutor(max_workers=1)
future = executor.submit(long_computation, 5)
future.add_done_callback(lambda f: print(f.result()))

print('waiting for callback')

executor.shutdown(False)  # non-blocking

print('shutdown invoked')

Esta é uma grande resposta, uma vez que é o único aqui que lhe dá a possibilidade de um pool de threads com retornos de chamada
Reda Drissi

Infelizmente, isso também sofre com o "Bloqueio global de intérpretes". Veja o doc da biblioteca: link . Testado com Python 3.7
Alex

0

Você pode usar o processo. Se você deseja executá-lo para sempre, use enquanto (como rede) em sua função:

from multiprocessing import Process
def foo():
    while 1:
        # Do something

p = Process(target = foo)
p.start()

se você quiser executá-lo apenas uma vez, faça o seguinte:

from multiprocessing import Process
def foo():
    # Do something

p = Process(target = foo)
p.start()
p.join()
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.