2

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.

  • Problem is, the event watching function (def run(self): below) is from a subclass, so I can't change it to async def run(self):. Therefore I can't use await channel.send()
  • My solution to this was to use run_coroutine_threadsafe like explained here: https://stackoverflow.com/a/53726266/9283107. That works good! But the problem is, the messages get put into a queue and they never get sent until this script finishes (which in my case would be: never). I assume the send message functions get put into the thread that this script is on, therefore the thread never gets to them?

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')
5
  • You're not allowed to call blocking code such as time.sleep() from an async def. What you should do is spawn a background thread (using something like threading.Thread(target=checker_function, args=(asyncio.get_event_loop(),)).start()) from on_ready or even from top-level code, where checker_function will execute Example().run(). 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/discord. Commented Oct 15, 2020 at 10:31
  • 1
    In summary, as I pointed out in a comment under the linked answer, asyncio.run_coroutine_threadsafe() assumes that you have multiple threads running (hence "thread-safe"), and here you obviously have only one. Until you change that, any attempt to use asyncio.run_coroutine_threadsafe() will fail. Commented Oct 15, 2020 at 10:32
  • This worked perfectly man! Thank you for the help and explanation, damn this stuff is quite confusing. Don't you want to post an official answer that I can mark for you? You can even copy and paste the answer I left below (I'll delete my answer) Commented Oct 15, 2020 at 11:00
  • I've now posted an answer that summarizes what I wrote in the comments, which you can accept if you like. You can also leave your answer, since it provides concrete working code, and thus a different perspective. Commented Oct 15, 2020 at 11:06
  • 1
    As for the confusion, I think it becomes less confusing if you remember that asyncio is really completely single-threaded (just like JavaScript, if you have experience with that). But you're still free to set up your own thread running legacy blocking code, in which case it's your responsibility to synchronize with the event loop. asyncio.run_coroutine_threadsafe is just a convenience function that can be called from another thread to "wake up" the event loop and tell it to do something. Commented Oct 15, 2020 at 11:13

3 Answers 3

6

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.

Sign up to request clarification or add additional context in comments.

Comments

1

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

1 Comment

Note that you don't need to use global variables to communicate with Example, you can use args=(...) to pass the event loop and the channel object to start_code, which can pass them to run(). That will make your code more reusable and easier to reason about.
0

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.