Noob level question: What is the difference betwee...
# coroutines
a
Noob level question: What is the difference between suspend functions and normal functions when calling from a coroutine? I mean, even if you call normal functions, they still run asynchronously, right?
j
I would say the opposite. When you're inside a coroutine, calling
suspend
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.
m
Calling a
suspend
function creates a suspension point. Calling a regular function will always execute in the same callstack
a
Yes, I should rephrase. Regardless of the
suspend
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. Thanks
e
when calling a suspend function, your current function will be suspended. for example,
Copy code
withContext(IO) { database() }
will free up the current thread to run other coroutines while IO is proceeding, while
Copy code
database()
will block the current thread if it is not a suspend function, even if it's using another thread to perform its work
a
So if we are in the main thread and execute:
CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>).launch { database() }
where database is NOT a suspend function It will still block the main thread?
I'm confused with what a suspension point really means. I'm assuming that whenever a suspend function call is made, that counts as a suspension point. And the program would first deal with the non-suspending code in the given order before dealing with the suspended calls.
j
Not sure what you mean by
CoroutineScope(<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.
๐Ÿ’ก 1
Ok I think I'm starting to understand your initial question, now.
Assuming you have this code:
Copy code
fun 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.
๐Ÿ’ก 1
a
Thank you for going in such detail. Then, in a nutshell, suspend is just sugar syntax for callbacks? 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. More 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. Is that correct?
๐Ÿ‘Œ 1
j
Expanding on the example above, things may behave slightly differently depending on the dispatchers used to run this code. Let's assume
doStuff()
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.
Then, 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.
๐Ÿ’ก 1
More 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:
Copy code
// 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).
๐Ÿ’ก 2
s
@Advitiay Anand Yup, suspend is like a callback that is called with a result (a value or Unit) or with an exception. That callback is a Continuation<T> The compiler allows the Continuation to appear on the left side of the expression (instead of on the right side as an input parameter), as a return value, allowing for a sequential/imperative programming style. suspend fun someFun(...): T is rewritten as fun someFun(..., Continuation<T>)
๐Ÿ‘€ 1
j
This is still the best source to understand this IMO if you're interested:

https://www.youtube.com/watch?v=YrrUCSi72E8โ–พ

๐Ÿ™ 1
s
That video is great, quite detailed! ๐Ÿ˜€
j
โ˜๏ธ yep that doc too, although it goes into even more details, so brace yourself!
e
there are callbacks but it's not the type of continuation-passing you would write by hand. each suspend function is turned into a state machine which will proceed to the next suspension point each time it is resumed, as well as being able to both call functions inline or dispatch them
s
Yup, seeing it as callbacks is a very simplified view. ๐Ÿ˜€ But that's the 10,000 feet view of 'suspend' for me. That a 'suspend' becomes Continuation is just one aspect. The guarantee that suspend funs can be called sequentially/imperatively in a safe way needs much more, like the state machine (with 'goto' statements) you describe.
e
yep, the "Continuation" aspect really shouldn't matter for how you use coroutines. suspension points as locations in the source code where execution can switch from one callstack to another, but otherwise everything that looks like sequential code will execute sequentially
๐Ÿ’ฏ 1