Ejecutando run_coroutine_threadsafe en un hilo separado

Oct 15 2020

Tengo un script que se ejecuta constantemente para siempre (verifica los cambios en los archivos). Necesito enviar mensajes de Discord cada vez que se crea un archivo extraño.

  • El problema es que la función de observación de eventos (a def run(self):continuación) es de una subclase, por lo que no puedo cambiarla a async def run(self):. Por lo tanto no puedo usarawait channel.send()
  • Mi solución a esto fue usar run_coroutine_threadsafecomo se explica aquí:https://stackoverflow.com/a/53726266/9283107. ¡Eso funciona bien! Pero el problema es que los mensajes se ponen en una cola y nunca se envían hasta que finaliza este script (que en mi caso sería: nunca). Supongo que las funciones de envío de mensajes se colocan en el hilo en el que se encuentra este script, por lo que el hilo nunca llega a ellas.

¿Quizás podamos lanzar el run_coroutine_threadsafeen un hilo separado o algo así? Este es el ejemplo más mínimo que puedo hacer que todavía muestra mi problema de subclase.

import discord
import os
import asyncio
import time

# CHANNEL_ID = 7659170174????????
client = discord.Client()
channel = None

class Example():
    # Imagine this run comes from a subclass, so you can't add sync to it!
    def run(self):
        # await channel.send('Test') # We can't do this because of the above comment
        asyncio.run_coroutine_threadsafe(channel.send('Test'), _loop)
        print('Message sent')

@client.event
async def on_ready():
    print('Discord ready')
    global channel
    channel = client.get_channel(CHANNEL_ID)

    for i in range(2):
        Example().run()
        time.sleep(3)

    print('Discord messages should appear by now. Sleeping for 20s to give it time (technically this would be infinite)')
    time.sleep(20)
    print('Script done. Now they only get sent for some reason')

_loop = asyncio.get_event_loop()

client.run('Your secret token')

Respuestas

2 user4815162342 Oct 15 2020 at 11:05

Primero, tenga en cuenta que no puede llamar a códigos de bloqueo como, por ejemplo, time.sleep()desde un async def. Para iniciar una función de bloqueo y hacer que se comunique con asyncio, puede generar un hilo en segundo plano desde on_readyo incluso desde el nivel superior, como este:

# checker_function is the function that blocks and that
# will invoke Example.run() in a loop.
threading.Thread(
    target=checker_function,
    args=(asyncio.get_event_loop(), channel)
).start()

Su hilo principal ejecutará el bucle de eventos de asyncio y su hilo de fondo comprobará los archivos, utilizando asyncio.run_coroutine_threadsafe()para comunicarse con asyncio y discord.

Como se señaló en un comentario debajo de la respuesta a la que se vinculó, se asyncio.run_coroutine_threadsafesupone que tiene varios subprocesos en ejecución (por lo tanto, "seguro para subprocesos"), uno de los cuales ejecuta el ciclo de eventos. Hasta que no implemente eso, cualquier intento de uso asyncio.run_coroutine_threadsafefallará.

2 Frank Oct 15 2020 at 10:59

Después de los user4815162342comentarios sobre la pregunta, se me ocurrió esto, ¡que funciona perfectamente!

import discord
import os
import asyncio
import time
import threading

CHANNEL_ID = 7659170174????????
client = discord.Client()
channel = None

class Example():
    # Imagine this run comes from a subclass, so you can't add sync to it!
    def run(self):
        # await channel.send('Test') # We can't do this because of the above comment
        asyncio.run_coroutine_threadsafe(channel.send('Tester'), _loop)
        print('Message sent')

def start_code():
    for i in range(2):
        Example().run()
        time.sleep(20)

@client.event
async def on_ready():
    print('Discord ready')
    global channel
    channel = client.get_channel(CHANNEL_ID)

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

_loop = asyncio.get_event_loop()

client.run('Your secret token')