Advitiay Anand
04/14/2022, 9:37 AMJoffrey
04/14/2022, 9:39 AMsuspend
functions and regular functions feels the same - they are run sequentially, in a manner that appears synchronous as far as the code is concerned (the next line of code will be executed after the function returns, and can access the return value of the called function).
If you're calling a suspend function, the current thread may be freed to go execute some other work (we call this a suspension point because the coroutine may be suspended when making this call), so it's behaving asynchronously behind the scenes despite the synchronous aspect of the code. However, if you call a regular function, the thread is actually blocked until the function returns, so there is a difference.mbonnin
04/14/2022, 9:41 AMsuspend
function creates a suspension point. Calling a regular function will always execute in the same callstackAdvitiay Anand
04/14/2022, 9:45 AMsuspend
keyword, the code inside a coroutine is run sequentially.
However, because we're inside a coroutine, the entire block of code can be run on a different thread (using Dispatchers)
For common use cases, like Room, shouldn't this be enough? How does using suspend
make a difference?
An example would be very much preferable. Thanksephemient
04/14/2022, 10:19 AMwithContext(IO) { database() }
will free up the current thread to run other coroutines while IO is proceeding, while
database()
will block the current thread if it is not a suspend function, even if it's using another thread to perform its workAdvitiay Anand
04/14/2022, 11:30 AMCoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>).launch { database() }
where database is NOT a suspend function
It will still block the main thread?Advitiay Anand
04/14/2022, 11:33 AMJoffrey
04/14/2022, 11:40 AMCoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>) { database() }
, as this doesn't compile.
If you meant CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>).launch { database() }
then you're creating another coroutine here, so things are much different. launch
is not a suspend function itself, and it's actually how you make asynchronous calls, so the code after the call to launch
will run concurrently with database()
. The main thread would not be blocked, but the current coroutine wouldn't even be suspended anyway.
If you meant withContext(<http://Dispatchers.IO|Dispatchers.IO>) { database() }
, like @ephemient suggested above, it's a different story. withContext
itself is suspending, so the current coroutine (which calls withContext
) suspends when making the call (that's a suspension point), and the code after withContext
will only run once the database()
call is done. However, that doesn't mean the main thread will be blocked. Instead, the main thread just stops executing this specific coroutine, but it can run other coroutines while the current coroutine is waiting for the result of database()
. Once database()
completes (on whatever IO thread it was using), the main thread can resume executing the initial coroutine.Joffrey
04/14/2022, 11:44 AMJoffrey
04/14/2022, 11:48 AMfun doStuff() {
someScope.launch {
someFunction()
}
doStuffConcurrently()
}
It doesn't matter (much) whether someFunction
is suspend or not - I think that was your question. What will happen either way here is that launch
is called, and doStuffConcurrently()
will run concurrently with whatever is inside the launch { ... }
block.Advitiay Anand
04/14/2022, 11:56 AMJoffrey
04/14/2022, 12:01 PMdoStuff()
is called on the main thread, and someScope
has a dispatcher that runs stuff on the Dispatchers.Main.immediate
(which is what happens if you use Android's built-in scopes like lifecycleScope
or viewModelScope
).
If someFunction
is not suspend, it has to block the thread running the launch
block until it completes, which means blocking the main thread here. Because of Dispatchers.Main.immediate
the launch body will not only run on the main thread but also run first (before doStuffConcurrently
). So even though doStuffConcurrently()
theoretically runs concurrently with the launch
body, it will not be able to run until someFunction
is done in this specific case due to thread starvation (because someFunction
block the main thread). That's why if someFunction()
does blocking I/O operations, it's better to override the dispatcher to make sure it runs on another thread or pool of threads. This is what <http://DIspatchers.IO|DIspatchers.IO>
is for.
Now, if someFunction
is suspend
, the call to it is a suspension point in itself, but also someFunction
itself may contain suspension points inside (if it calls other suspend functions). Having multiple suspension points allows pieces of code from concurrent coroutines to interlace even when run on the same thread.Joffrey
04/14/2022, 12:06 PMThen, in a nutshell, suspend is just sugar syntax for callbacks?You can see it this way, yes. It's a way to write async code in a sequential way, when it would usually use callbacks. It's not implemented with actual callbacks, though, but with state machines instead.
And suspension points have nothing to do with multi-threaded programming. Rather they only change the order in which the code is executed, albeit on the same thread.Suspension points decide when a coroutine can suspend. Suspending a coroutine means that the coroutine is set aside and frees the thread that was running it, so that thread can go execute other coroutines. You can see suspension points as a way to slice the coroutine's code into pieces (like sub-tasks), and those pieces are sort of the units that are executed by a thread. Those sub-tasks are guaranteed to run in the order they appeared in the coroutine, but they are not guaranteed to run on the same thread. If the dispatcher running the coroutine happens to be a multi-threaded dispatcher, each sub-task can theoretically be run by a different thread. Sorry if that sounds very theoretical, maybe an example would help better.
Joffrey
04/14/2022, 12:14 PMMore importantly, even if we call a suspend function on the main thread, it only blocks that specific coroutine but leaves the thread open to run other coroutines concurrently.In general yes it's a good rule of thumb. Well-behaved suspend functions should be main-friendly. However, in theory it depends on the implementation of the suspend function. Adding the
suspend
keyword doesn't magically solve problems. For instance:
// don't do this!
suspend fun notReallySuspend() {
Thread.sleep(2000)
}
This function is marked suspend
but it actually still blocks the thread. The IDE would probably warn you here that suspend
is redundant. Actual well-behaved suspend functions should instead either call other suspend functions (the most common), or explicitly suspend the coroutine and resume it (often when integrating with callback-based APIs).streetsofboston
04/14/2022, 12:17 PMstreetsofboston
04/14/2022, 12:20 PMephemient
04/14/2022, 12:20 PMJoffrey
04/14/2022, 12:21 PMephemient
04/14/2022, 12:24 PMstreetsofboston
04/14/2022, 12:30 PMephemient
04/14/2022, 12:32 PM