https://kotlinlang.org logo
#coroutines
Title
# coroutines
m

Martin Gaens

07/09/2022, 5:56 PM
This might be a little off-topic but I decided to give it a shot. I'm using a Kotlin Telegram bot framework from here. Now, the framework allows you to specify callback functions for certain actions. For example, if your Telegram bot receives a message, you can tell the bot to respond to the message. Here's the code:
Copy code
fun main() = runBlocking {
    val bot = bot {
        token = "YOUR_API_KEY"
        dispatch {
            text {
                launch { println("launched") }
                bot.sendMessage(ChatId.fromId(message.chat.id), text = text)
            }
        }
    }
    bot.startPolling()
}
This code should set a callback for when the bot receives any text message from a user. However, the
launch { }
never gets launched. And I don't know why! I know all this Telegram bot stuff might be off-topic but it's related to coroutines and I can't comprehend why it wouldn't work. Can somebody tell me what I'm doing wrong?
e

ephemient

07/09/2022, 6:22 PM
scheduling is cooperative. since
startPolling
is a blocking function, no other coroutines in this thread can run until it returns
m

Martin Gaens

07/09/2022, 6:23 PM
So the solution is to run
startPolling
in a coroutine?
Putting the
startPolling
into a
launch { }
block doesn't seem to help.
j

Joffrey

07/09/2022, 6:31 PM
This extra coroutine will still hold the only thread available, because
runBlocking
holds the main thread, and provides a scope that's an event loop on that thread. If you pass a multithreaded dispatcher to it, it should solve the problem
m

Martin Gaens

07/09/2022, 11:39 PM
I've been messing around with this for a while with no luck. How exactly would I create a multithreaded dispatcher and then run the stuff using it?
e

ephemient

07/10/2022, 1:52 AM
`Dispatchers.IO`/`Dispatchers.Default` are built-in, and on JVM you can create your own multi-threaded
Executor
via the usual means and wrap it with
.asCoroutineDispatcher()
m

Martin Gaens

07/10/2022, 7:38 AM
I should really dive deep into coroutines. Very interesting, I didn't know these two specifically were multithreaded. Thank you very much, I'll give it a shot.
I s*till* can't figure it out 😕 nothing is working. The bot gets run and after that, no coroutines run anymore. And as I checked, they should be running on different threads. I even tried to wrap the
bot.startPolling()
inside a
thread(start = true) { }
with no luck. Here's the code:
Copy code
fun log(msg: String?) = println("[${Thread.currentThread().name}] $msg")

fun main() = runBlocking {
    val bot = bot {
        token = "XXX"

        dispatch {
            command("sayHi") {
                launch(<http://Dispatchers.IO|Dispatchers.IO>) {
                    log("Hey")
                    bot.sendMessage(
                        chatId = ChatId.fromId(message.chat.id),
                        text = "Hi!"
                    )
                }
            }
        }
    }

    launch(Dispatchers.Default) {
        log("Started polling...")
        bot.startPolling()
    }

    Unit
}
Also are there any good resources for learning about coroutines? Because when you google "create multithreaded executor", it doesn't bring up anything useful and easy to understand about creating multi-threaded executors.
u

uli

07/10/2022, 2:38 PM
In your Original code, pass a dispatcher to
launch
and see if that does the trick.
j

Joffrey

07/10/2022, 2:45 PM
If you pass a multithreaded dispatcher to it, it should solve the problem
@Martin Gaens I meant to pass a dispatcher to
runBlocking
- not to the
launch
directly. The thing is, because you run your top-level code in
runBlocking
, the thread is blocked while waiting for the second
launch
to finish, even if the body of that
launch
is run on a different thread. Everything needs to be multithreaded here, not just the body of
launch
.
Looking at your initial code again, it doesn't seem right to use
runBlocking
at that level. Everything is blocking here, except your own coroutine call in
launch
. By capturing `runBlocking`'s scope in the callback's lambda instead of using
runBlocking
inside the callback, you're possibly breaking invariants for this callback. Maybe the framework expects you to finish doing your work in the callback before giving control back to the framework. It also probably gives you a separate thread to do that, but you're switching back to the thread held by
runBlocking
(which is the main thread in this case).
u

uli

07/10/2022, 6:01 PM
From the sample on the framworks readme.md it looks like all the framework code is supposed to run un the main thread (or thread provided by framework for the callback) and it is ok to do blocking work. So your initial example looks correct, except for the fact that you launch your coroutine on the main thread which is blocked. Did you get to try launching the coroutine in the background? And only putting the println inside the coroutine? Could be that the framework is not thread safe and calling framework code from the background breaks things. Btw. why do you want coroutines any way? It looks like it’s fine to do all your work on the main thread (or the one that your callback is called on).
Copy code
fun main() = runBlocking {
    val bot = bot {
        token = "YOUR_API_KEY"
        dispatch {
            text {
                println("received") // Check if your callback is called at all
                launch(<http://Dispatchers.IO|Dispatchers.IO>) { println("launched") } // See if couroutine gets launched
                bot.sendMessage(ChatId.fromId(message.chat.id), text = text) // Do not call framework from background thread
            }
        }
    }
    bot.startPolling()
}
i disclaim all that has been said and assert the contrary 🤣
The issue is with `startPolling()`is not blocking the main thread. It schedules a run loop in a background thread and then returns immediately. After that execution leaves runBlocking’s scope. This completes/cancels the scope’s job and prevents any new coroutines from being started. StartPolling’s run loop is probably scheduled on a non daemon thread which prevents the JVM from terminating after main exits. So this should do the trick:
Copy code
fun main() {
    cal scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
    val bot = bot {
        token = "YOUR_API_KEY"
        dispatch {
            text {
                println("received") // Check if your callback is called at all
                scope.launch { println("launched") } // See if couroutine gets launched
                bot.sendMessage(ChatId.fromId(message.chat.id), text = text) // Do not call framework from background thread
            }
        }
    }
    bot.startPolling()
}
m

Martin Gaens

07/10/2022, 8:21 PM
In your Original code, pass a dispatcher to
launch
and see if that does the trick.
The
<http://Dispatchers.IO|Dispatchers.IO>
dispatcher is not working if I pass it into the original code.
I meant to pass a dispatcher to
runBlocking
Yeah I also tried that, also not working. @Joffrey thanks for the answer, you're right that the framework expects me to do all the work in the thread, but I don't really like holding onto a thread when I'm simply waiting for a Ktor response. That's why I wanted to go full coroutine-based. But when the framework doesn't support coroutines, I guess my only option to achieve this goal is to make my own... 😛 @uli thanks for your detailed answers! I tried doing a simple
println()
inside a coroutine with no luck. The coroutine simply wasn't called at all. Also, the first thing you proposed didn't work. The second one with the custom scope worked! But it's still very annoying to me that the framework creates threads when coroutines are available to us in Kotlin and are preferable for doing simple network calls. However, why shouldn't I call the framework from a background thread/coroutine?
u

uli

07/10/2022, 8:53 PM
If the framework does not state any thing about thread safety a conservative assumption is it is not thread safe. That might be wrong and you might be just fine calling it from a coroutine thread. My advise was mainly while you try to get it running to prevent any thread related issues. The assumption was also based on the (false) assumption, that the framework runs on the main thread. An already mutli threaded framework is much more likely to be thread safe :-) Now that it runs feel free to try calling the bot from your own threads!
The framework itself not using coroutines seems more like an implementation detail. Why would you care? You can still use coroutines for your purpose. And coroutines would become important if the framework had to serve thousands of connections. My assumption is this is not the case here.
m

Martin Gaens

07/10/2022, 9:10 PM
Yeah I've seen a few `@Volatile`s spread out in the framework, so I'm guessing we can assume it is thread safe. And yeah I guess the thread implementation detail is a small detail but I think it's still much better using coroutines because I'm planning on deploying on not-so-good hardware where there's a very limited amount of threads available.
g

gildor

07/11/2022, 4:37 PM
I honestly don't see why you need runBlocking here at all, just create your own scope (application level or/and request level) or even use GlobalScope and run coroutine And what is your goal here? To use coroutines to run multiple tasks in parallel
Answering your original question directly, just remove runBlocking, replace launch with GlobalScope.launch But the actual solution depends on what kind of lifecycle you expect from your coroutines