Hi there,
I’m working on a bot to do social games on the fedi, and using the mastodon-async crate for communicating with the ActivityPub server in question. At the moment I’m using tokio mt as a runtime, though I’m new at async so if you think I shouldn’t let me know.
The pattern I want to implement is the following:
- At any given time, a user sends a “play” message to the bot.
- If the player list is empty, the player is added to it awaiting someone else.
- Otherwise, the bot checks if there are enough players on its list (who have previously sent a play message). For some games, enough is 1, since it’s a 2-player game, for some it’s 3 or more.
- If there are enough players, play commences. list is cloned for that match, then emptied so other players can get in.
What I’m not very clear is how to keep this list to assure that sequence will be respected. I.a., if two play messages come reasonably quick together, I want one to be processed, then entered on the list, or get the match to start; then the other to get processed.
My current thoughts:
- I could use a channel that receives the player accounts. When a new player is added, it performs the logic.
- I could use a mutex with a list (or an option player value for the degenerate case of 2-player games).
Any thoughts on what the reasonable thing to do is here? I’m very new to async and while I realise there’s probably lots of ways to do this, they’re not all equally ergonomic and I want to avoid myself future pain.
Both would work. The channel-based one is probably more ergonomic, because there you can write the sequence linearly just as you described.
One important thing for both approaches is that you must not use blocking variants of the mutex or channel. There are async versions of both available, though.
So probably the tokio mpsc channel, right? Why is it not possible tu use normal sync channels? I’ve read about it but I don’t understand the reason.
Also I’m thinking of spawning a thread to do this part, or should it run on the tokio main function?
tokio uses thread pools for scheduling async tasks, which generally is what you want (because spawning threads is expensive and can lead to DoS vulnerabilities).
If you block in an async task, the thread the task is running on is no longer available to other async tasks. If you have the same amount of tasks currently blocking as you have threads, the whole system grinds to a halt.
The regular versions are blocking. If an item isn’t available they will put the current OS thread to sleep and wait for the channel or mutex.
This sort of defeats the purpose of async which is to allow many logical threads but actually consumes a small number of OS threads.
When you wait on an async channel or mutex instead of blocking the current thread it will yield to the current executor. This will allow the current OS thread to run other tasks to run in the meantime. Then when the channel has an item or the lock is unlocked the executor will then continue running the original future from where it left off.
It’s worth noting that on most major desktop executors/async runtimes this won’t actually cause any problems. They will spawn a new OS thread to run other tasks if you block one. This will use a bit more memory and maybe a small latency blip but ultimately not be a major issue. However some runtimes will have a fixed number of backing OS threads and blocking them will result in higher latency or deadlocks. So in general you shouldn’t block in async functions.