Frank
Frank

Reputation: 2431

Executing run_coroutine_threadsafe in a separate thread

I have a script that is constantly running forever (it checks changes in files). I need to send Discord messages whenever a weird file is made.

Maybe we can throw the run_coroutine_threadsafe into a separate thread or something? This is the most minimal example I can make that still shows my subclass problem.

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')

Upvotes: 2

Views: 3387

Answers (3)

Xewid
Xewid

Reputation: 1

I think you can also do:

client.loop.create_task(channel.send('Test'))

instead of await channel.send('Test').

But the coroutine channel.send('Test') might take a few seconds before starting to execute whereas asyncio.run_coroutine_threadsafe immediately executes it, which is a difference I struggle to understand these last days.

Upvotes: 0

user4815162342
user4815162342

Reputation: 154846

First, note that you're not allowed to call blocking code such as time.sleep() from an async def. To start a blocking function and have it communicate with asyncio, you can spawn a background thread from on_ready or even from top-level, like this:

# 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()

Your main thread will run the asyncio event loop and your background thread will check the files, using asyncio.run_coroutine_threadsafe() to communicate with asyncio and discord.

As pointed out in a comment under the answer you linked to, asyncio.run_coroutine_threadsafe assumes that you have multiple threads running (hence "thread-safe"), one of which runs the event loop. Until you implement that, any attempt to use asyncio.run_coroutine_threadsafe will fail.

Upvotes: 6

Frank
Frank

Reputation: 2431

Following user4815162342 comments on the question, I came up with this, which works perfectly!

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')

Upvotes: 1

Related Questions