-
Notifications
You must be signed in to change notification settings - Fork 1.9k
update: rewriting coroutines basics for coroutines guide updates #4474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I unfortunately only lasted for 200 lines, though 🙂 I mostly left wording-related comments in the PR.
I do have thought on the general structure and I can produce more wording suggestions, just don't have more time to think them through today.
|
||
Let's dissect what this code does. | ||
To include the `kotlinx.coroutines` library in your project, add the corresponding dependency configuration based on your build tool. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another way to add kotlinx.coroutines
to your project is to add something from the coroutine library and follow IDE's prompts.
Would be nice to provide it as the first option: "create an empty Kotlin project in Idea with the default settings, copy paste this example (todo) and follow the prompts (specify which, there should be two: first to include the kotlinx.coroutines dependency, then to include a coroutine import) to include"
I assume a dominant part of users who need to read this basic guide will be using Idea, so why not to make it easy for them. And for those who don't use Idea, that could be yet another way to advertise that everything works out of the box!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have several concerns about this:
- Anything that's in the official documentation becomes a promise to keep. We need to make sure the IDE team considers this functionality reliable before making that promise on their behalf. Otherwise, we risk users getting frustrated and wasting time on functionality that only works part of the time.
- Many (most?) users reading this are using Android Studio, which is based on IntelliJ, but is not fully controlled by us. We'd need to check that it works there as well.
- People may try to transfer this advice to their own project, with a far more complex configuration than the empty one. Gradle configurations are just arbitrary code in general, so it's impossible for the IDE to figure out how to add dependencies reliably in all cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also think we shouldn't cover something like this. Usually, we only include such information if it's an IDE specific plugin (for example, Kotlin Notebook)
|
||
## Create your first coroutines | ||
|
||
Suspending functions are the basic building blocks for concurrency in Kotlin, but they can only run inside another suspending function or a coroutine. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: but
is misleading, there isn't any contrast, these two statements are not contradicting to each other. Breaking it down into two equal sentences simplifies reading complexity:
Suspending functions are the basic building blocks for concurrency in Kotlin. A suspending function can only run inside another suspending function or a coroutine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or
is also misleading. I'd just remove or a coroutine
entirely.
Threads run functions, from which one can call other functions; coroutines run suspend
functions, from which one can call other (possibly suspend
) functions. "Functions can only run inside other functions or threads" sounds very strange to me: they usually run in both.
## Create your first coroutines | ||
|
||
Suspending functions are the basic building blocks for concurrency in Kotlin, but they can only run inside another suspending function or a coroutine. | ||
A coroutine is a suspendable computation that runs on _threads_ and can run concurrently or in parallel with other coroutines. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very subtly introducing the notion of concurrent vs parallel. While I don't think it is the focus here to allow digressing into the difference, it would generally be nice to have maybe a separate link / section describing this?
I like this picture from Kotlin for Education lecture slides.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I feel like this intro or a subsection needs a similar picture for threads vs coroutines, but I couldn't find anything simple enough. Not comparing coroutines to threads, but showing how coroutines are run on top of threads.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To add to this,
concurrently or in parallel
I'd say all parallelism (executing tasks simultaneously in time) is concurrency (executing several tasks without waiting for one to finish before starting another), but not vice versa, so I'd phrase it a bit differently, like "and run concurrently with other coroutines (possibly in parallel)".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔
I like this picture from Kotlin for Education lecture slides.
I think we could introduce a similar picture at the very beginning of the document where we first talk about concurrency, I'll try to scetch up something and see how it flows! 👍
and run concurrently with other coroutines (possibly in parallel)
I like it — let's go with that version!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re pictures - just to clarify, I mentioned two pictures:
- coroutines vs threads (in terms of how coroutines run on threads)
- concurrent vs parallel
I feel like the first one is more needed than the second one, although both would be nice to show up at some point (maybe in other sections)
In any case I'd love to see what you come up with!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"on threads" here seems redundant to me, given the more elaborate explanation just below.
A coroutine is a suspendable computation that runs on _threads_ and can run concurrently or in parallel with other coroutines. | ||
|
||
On the JVM, all concurrent code, such as coroutines, runs on threads, which are managed by the operating system. | ||
Coroutines can suspend their execution instead of blocking a thread, which lets you run many tasks with less overhead. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May I suggest a more specific example illustrating the same concept ("coroutines are effective").
While one coroutine is suspended, for example, waiting for a network package to arrive, another coroutine can execute on the same thread, ensuring effective resource utilization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, sounds like a great idea — I like it! 👍 Let's add it
6. Combine these pieces to run multiple coroutines at the same time on a shared pool of threads: | ||
|
||
```kotlin | ||
// Imports the coroutines library |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm used to seeing imperative (import instead of imports) in comments and docs: import the library, define something, launch something else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using third person in docs for code comments is part of the style guide, see here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh. I still hold that the third-person "imports, defines, launches" are a bit annoying. What everyone thinks?
Is there any room to wiggle on this one? Since it's not documentation per se, it's a tutorial / guide. Also, current guide uses a mix of imperative and third person.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can give instructions as code comments like "// Replace this part with your dependencies"
Or we can give additional info like "// Main.kt "
But in this context, I don't see a reason to deviate from the style here. 🤔 We want to ensure that users have the same experience when interacting with documentation, regardless of the topic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm reading these comments as an instruction to the reader on what to do: you should import, then you should run the coroutines, etc.
I just want to register my disagreement, and agree to disagree.
// Runs code in the coroutine scope | ||
println("This runs while the coroutine is active") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what is meant by "This runs while the coroutine is active". This wording ("while ... active") misleads me into thinking that this println will be repeated multiples times while other things are running.
What does this bit aims to demonstrates?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm 🤔
I wanted to demonstrate how .launch()
is not blocking the scope and that this part runs/finishes before those.
The issue is that I didn't want to go into too much detail here because we haven't introduced terms like child coroutines...
Looking at it, since we have a similar example demonstrating this behavior further down in the Coroutine scope and structured concurrency we could modify that part to something else, like "This runs alongside the launched coroutine".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something along the lines of:
"This print runs after launching the two coroutines above, concurrently with them, without waiting for them to complete."
Coroutines follow a principle of _structured concurrency_, which means new coroutines can only be launched in a specific [`CoroutineScope`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/) that defines their lifecycle. | ||
|
||
When you work with multiple coroutines, you often need to run many tasks at once, cancel them when they are no longer needed, and handle errors safely. | ||
Without clear structure, you risk leaving coroutines running longer than needed, wasting resources. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One also risks not having error propagation. I'd say leave out this sentence completely, since it's basically the negation of the previous one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Alternatively, it could be enhanced by mentioning that not handling errors properly may lead to the program entering an inconsistent state. Then, this would be not just a negation but an elaboration in specific terms on what dangers structured concurrency saves you from.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed it 👍
|
||
## Coroutine scope and structured concurrency | ||
|
||
Coroutines follow a principle of _structured concurrency_, which means new coroutines can only be launched in a specific [`CoroutineScope`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/) that defines their lifecycle. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While "new coroutines can only be launched in a specific CoroutineScope" is true, it is only a property and also an implementation detail of structured concurrency.
For me, structured concurrency means that coroutines (computations) are arranged into a forest of trees (coroutines form parent-child relationships, and multiple coroutines can be the roots of computations) which allows for (enables) cancellation support and error propagation.
Structured concurrency is a very big concept, and the definition should be grand and catchy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then introducing CoroutineScope could go like this:
(Insert a sentence here to ligate structured concurrency to CoroutineScop). Each coroutine is launched within a single CoroutineScope that defines/manages its lifecycle.
I'm not sure about the word lifecycle, too. Does it mean structuring + cancellation + error in this context? If so, maybe it's worth elaborating that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a very good point... let's restructure it, what do you think of something like this:
In this version we first introduce the parent-child relationships — also we focus on the cancellation part as well then we tie it all up with the part that to ensure that all coroutines are launched from a CoroutineScope.
When you run many coroutines in an application, you need a way to manage them as a group.
Kotlin coroutines rely on a principle called structured concurrency to provide this structure.This principle connects coroutines into a hierarchy of parent and child tasks that share the same lifecycle.
A parent coroutine waits for its children to complete before finishing.
If the parent coroutine fails or is canceled, all its child coroutines are canceled too.
Keeping coroutines connected in this way makes cancellation, error handling, and resource cleanup predictable and safe.To keep this structure, new coroutines can only be launched in a
CoroutineScope
that defines and manages their lifecycle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like the word lifecycle as it is used as a new term, without being properly introduced and defined. I still don't know what exactly you mean by lifecycle?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, what do you think about adding this before the sentence starting with "A parent coroutine..."?
A coroutine’s lifecycle is the period from its start until it completes, fails, or is canceled.
|
||
Coroutines follow a principle of _structured concurrency_, which means new coroutines can only be launched in a specific [`CoroutineScope`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/) that defines their lifecycle. | ||
|
||
When you work with multiple coroutines, you often need to run many tasks at once, cancel them when they are no longer needed, and handle errors safely. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's another way around. You utilize coroutines when you need to run many tasks, cancel them, and handle errors. Running tasks is the goal, while coroutines are just the means to do so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point 👍 I revised this section, see the comment where we talk about the introduction of structured concurrency.
|
||
You will see the following result: | ||
This example doesn’t use concurrency yet, but by marking the functions with the `suspend` keyword, | ||
you allow them to be used in concurrent code later. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not quite. suspend
functions allow using concurrency inside them, but you can call non-suspend
functions from suspend
ones freely. Concurrent code is free to use everything, but can only be used from inside suspend
functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about:
This example doesn’t use concurrency yet, but by marking the functions with the suspend
keyword,
you allow them to call other suspending functions and run concurrent code inside.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that works.
## Create your first coroutines | ||
|
||
Suspending functions are the basic building blocks for concurrency in Kotlin, but they can only run inside another suspending function or a coroutine. | ||
A coroutine is a suspendable computation that runs on _threads_ and can run concurrently or in parallel with other coroutines. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To add to this,
concurrently or in parallel
I'd say all parallelism (executing tasks simultaneously in time) is concurrency (executing several tasks without waiting for one to finish before starting another), but not vice versa, so I'd phrase it a bit differently, like "and run concurrently with other coroutines (possibly in parallel)".
often see `runBlocking` used like that at the very top-level of the application and quite rarely inside the real code, | ||
as threads are expensive resources and blocking them is inefficient and is often not desired. | ||
> You can display coroutine names next to thread names in the output of your code for additional information. | ||
> In IntelliJ IDEA, right-click the `Run` icon: {width=30} next to your `main()` function, select `Modify Run Configuration...`, and add `-Dkotlinx.coroutines.debug` in `VM options`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unrelated to IDEA, though. You can just as well pass -Dkotlinx.coroutines.debug
directly to Maven or Gradle in a console invocation or anywhere else. I think it makes sense to avoid mentioning IDEA here and make the advice universally applicable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea 👍
Let's go with:
You can display coroutine names next to thread names in the output of your code for additional information.
To do so, pass the -Dkotlinx.coroutines.debug
VM option in your build tool or IDE run configuration.
> You can display coroutine names next to thread names in the output of your code for additional information. | ||
> In IntelliJ IDEA, right-click the `Run` icon: {width=30} next to your `main()` function, select `Modify Run Configuration...`, and add `-Dkotlinx.coroutines.debug` in `VM options`. | ||
> | ||
> See [Debug coroutines using IntelliJ IDEA — Tutorial](debug-coroutines-with-idea.md) for more information. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/topics/debugging.md is the file to link here, as it's the one describing kotlinx.coroutines.debug
.
> While you can mark the `main()` function as `suspend` in some projects, other projects or frameworks might not allow changing the `main()` function this way. | ||
> In those cases, coroutine builder functions like [`runBlocking`](#runblocking) are the usual entry point for calling suspending code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
runBlocking
is the only such bridging function, and I'd shift the focus from main()
specifically. I'm thinking of something like this:
While you can mark the
main()
function assuspend
in some projects, it may not be possible when integrating coroutines with existing code or building the project around a framework. In that case, check the documentation of the framework to see if it provides the ability to callsuspend
functions already. If all else fails, it is possible to runsuspend
functions by blocking the current thread by usingrunBlocking
.
A bit too verbose, but I think it's important to get the point across: after all, we are literally talking about entry points, nothing is possible without them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good approach 👍
3. Use the [`coroutineScope()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html) function to [create a `CoroutineScope`](#coroutine-scope-and-structured-concurrency) that groups your coroutines and controls their lifecycle: | ||
|
||
```kotlin | ||
suspend fun main() = coroutineScope { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that this series of examples follows the idea I've proposed of mentioning dispatchers early on, but as is, it's not quite the code that anyone would actually write. I was thinking of something like this (and could probably have been clearer about it):
suspend fun main() = withContext(Dispatchers.Default) {
launch { greet() }
println("This can run in parallel with the greeter coroutine")
}
This way, it's clear in which context all pieces of code inside main
are running, including the println
.
This example demonstrates simple multithreading with coroutines on a shared thread pool. | ||
|
||
> Try running the example multiple times. | ||
> You may notice that the output order and thread names may change each time you run the program. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They could have noticed that on step 4 already, by the way, as the launch
'ed coroutine runs in parallel with the code that launched it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true 👍 I'd prefer to keep the tip at the bottom though since thats where the user can run the code on the website.
Coroutines follow a principle of _structured concurrency_, which means new coroutines can only be launched in a specific [`CoroutineScope`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/) that defines their lifecycle. | ||
|
||
When you work with multiple coroutines, you often need to run many tasks at once, cancel them when they are no longer needed, and handle errors safely. | ||
Without clear structure, you risk leaving coroutines running longer than needed, wasting resources. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Alternatively, it could be enhanced by mentioning that not handling errors properly may lead to the program entering an inconsistent state. Then, this would be not just a negation but an elaboration in specific terms on what dangers structured concurrency saves you from.
|
||
Run the following code to get to your first working coroutine: | ||
You can only call a suspending function from another suspending function or from a coroutine. | ||
Even the `main()` function can be suspending: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why "even", though? The idea being highlighted here (I assume) is that suspend fun main
is one of the ways of obtaining the "root" suspend
function, which will then allow calling the other ones.
|
||
You will see the following result: | ||
This example doesn’t use concurrency yet, but by marking the functions with the `suspend` keyword, | ||
you allow them to be used in concurrent code later. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that works.
Hello | ||
World! | ||
``` | ||
While the `suspend` keyword is part of the Kotlin standard library, most coroutine features |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While the `suspend` keyword is part of the Kotlin standard library, most coroutine features | |
While the `suspend` keyword is part of the Kotlin language itself, most coroutine features |
The standard library does contain the basic building blocks used by kotlinx.coroutines
(for example, the suspendCoroutine
function), but keywords are not provided in the library.
|
||
## Create your first coroutines | ||
|
||
Suspending functions are the basic building blocks for concurrency in Kotlin, but they can only run inside another suspending function or a coroutine. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or
is also misleading. I'd just remove or a coroutine
entirely.
Threads run functions, from which one can call other functions; coroutines run suspend
functions, from which one can call other (possibly suspend
) functions. "Functions can only run inside other functions or threads" sounds very strange to me: they usually run in both.
|
||
## Scope builder and concurrency | ||
Use the [`.runBlocking()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) coroutine builder function to create a coroutine scope in a blocking context. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the [`.runBlocking()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) coroutine builder function to create a coroutine scope in a blocking context. | |
Use the [`runBlocking()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) coroutine builder function to create a coroutine scope and block the current thread until the coroutine started in it finishes. |
"Blocking context" is somewhat too abstracted from the practical consequences of using runBlocking
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, this is much clearer 👍 I rewrote this a little because this way the next sentence almost repeats this sentence, what do you think? 🤔
Use the runBlocking()
coroutine builder function to create a coroutine scope and block the current thread until
the coroutines launched in that scope finish.
Unlike other coroutine builders, runBlocking()
doesn't use a shared thread pool.
|
||
A [coroutineScope][_coroutineScope] builder can be used inside any suspending function to perform multiple concurrent operations. | ||
Let's launch two concurrent coroutines inside a `doWorld` suspending function: | ||
`.runBlocking()` creates a new `CoroutineScope` for the code inside it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
`.runBlocking()` creates a new `CoroutineScope` for the code inside it. | |
`runBlocking()` creates a new `CoroutineScope` for the code inside it. |
A [coroutineScope][_coroutineScope] builder can be used inside any suspending function to perform multiple concurrent operations. | ||
Let's launch two concurrent coroutines inside a `doWorld` suspending function: | ||
`.runBlocking()` creates a new `CoroutineScope` for the code inside it. | ||
This lets you call suspending functions without marking the surrounding function as `suspend`, for example, in the `main()` function: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This lets you call suspending functions without marking the surrounding function as `suspend`, for example, in the `main()` function: | |
It lets you call suspending functions without marking the surrounding function as `suspend`, for example, in the `main()` function: |
Where "it" = runBlocking
.
Otherwise, I'm reading this as "this property of creating a new CoroutineScope
allows calling suspend
functions". That would be incorrect: the reason we can run suspend
functions there is that runBlocking
's block
parameter has suspend
in its type.
|
||
```kotlin | ||
import kotlinx.coroutines.* | ||
|
||
//sampleStart | ||
// Sequentially executes doWorld followed by "Done" | ||
// Runs the greet() function inside a blocking scope | ||
fun main() = runBlocking { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indiscriminately using runBlocking
is an antipattern that we want to discourage: #4412
Instead, we can formulate an example that explains when runBlocking
is useful/unavoidable in real scenarios. It's something like this:
// a third-party library defines this interface that we must implement
interface Repository {
fun readItem(): Int
}
object MyRepository: Repository {
override fun readItem(): Int {
// we can't call suspend functions here,
// because the interface we must implement expects synchronous code.
// But we want to use coroutines as the implementation detail!
return runBlocking {
myReadItem()
}
}
}
suspend fun myReadItem(): Int {
delay(100.milliseconds)
return 4
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the example - let's go with that
## Coroutine dispatchers | ||
|
||
A [coroutine dispatcher](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/#) controls which thread or thread pool coroutines use for their execution. | ||
Coroutines are not tied to a single thread. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coroutines are not tied to a single thread. | |
Coroutines are not always tied to a single thread. |
They may be tied to a single thread. That's occasionally necessary. For example, Dispatchers.Main
ensures all code runs on the (single) UI thread.
A [coroutine dispatcher](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/#) controls which thread or thread pool coroutines use for their execution. | ||
Coroutines are not tied to a single thread. | ||
They can pause on one thread and resume on another, depending on the dispatcher. | ||
This lets you run many coroutines at the same time without blocking threads. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This lets you run many coroutines at the same time without blocking threads. | |
This lets you run many coroutines at the same time without allocating a separate thread for every coroutine. |
That's the point of sharing threads. They can still be blocked: withContext(Dispatchers.IO) { readFile(path) }
will block the OS thread that's reading the file, this is unavoidable.
A dispatcher works together with the [coroutine scope](#coroutine-scope-and-structured-concurrency) to define when coroutines run and where they run. | ||
While the coroutine scope controls the coroutine’s lifecycle, the dispatcher controls which threads are used for execution. | ||
|
||
> You don't have to create a dispatcher for every coroutine. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
> You don't have to create a dispatcher for every coroutine. | |
> You don't have to specify a dispatcher for every coroutine. |
Creating dispatchers is possible, but quite niche.
|
||
### Structured concurrency | ||
To create a coroutine in Kotlin, you need a suspending function, a scope, a builder to start it, and a dispatcher to control which threads it uses: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Obviously under the hood, the coroutine has an associated dispatcher, but I'm wondering if it's still right to say that "to create a coroutine, you need a dispatcher"? After all, from the developer's perspective, you can happily omit it.
|
||
suspend fun main() = coroutineScope { | ||
// Starts a coroutine with Dispatchers.Default | ||
launch(Dispatchers.Default) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that there's no dispatcher to inherit, even the unparametrized version launches on Dispatchers.Default
. I'm guessing you picked Default
here because anything else would require too much storytelling to justify?
Might I suggest adding the word "explicitly" to the comment above this line, to make this more clear?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea! I'll add it 👍
} | ||
|
||
suspend fun main() = coroutineScope { | ||
// Starts a coroutine with Dispatchers.Default |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a question regarding the preposition: I've often heard it referred to as running on a Dispatcher (since that's what defines the shared pool of threads) rather than with a Dispatcher. Thoughts on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 You are right - it feels off re-reading it. I like running on a Dispatcher as a general solution in this case and in similar cases 👍
Here I think either could work :
- Starts a coroutine that runs on Dispatchers.Default (I prefer this for its conciseness)
- Starts a coroutine using Dispatchers.Default to control where it runs/choose the thread pool etc.
launch { doWorld() } | ||
println("Hello") | ||
} | ||
suspend fun main() = coroutineScope { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you share the reasoning behind this pattern / where I could learn more about it?
|
||
For more information, see [Coroutine context and dispatchers](coroutine-context-and-dispatchers.md) | ||
|
||
## Comparing coroutines and JVM threads |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like it'd be a good place to reassure people about the fact that despite potentially running on different carrier threads, there's a clear happens-before relationship inside an individual coroutine, and that a coroutine -- even when hopping between threads -- is guaranteed to observe any preceding writes (i.e. that despite thread-hopping, that there are no "mutable shared state" issues)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea, what do you think about adding a tip before the link with something like this:
Even though coroutines can suspend and resume on different threads, values written before the coroutine suspends are still available within the same coroutine when it resumes.
```kotlin | ||
import kotlinx.coroutines.* | ||
|
||
fun main() = runBlocking { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guessing that given the prev. discussion, this is also a place where runBlocking should go away.
{style="note"} | ||
|
||
|
||
3. Use the [`coroutineScope()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html) function to [create a `CoroutineScope`](#coroutine-scope-and-structured-concurrency) that groups your coroutines and controls their lifecycle: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@murfel raised an interesting point regarding this: for newcomers to Kotlin that go straight to learning coroutines, the mechanism with which coroutineScope
provides a CoroutineScope
inside it is unclear. A link to https://kotlinlang.org/docs/lambdas.html#function-literals-with-receiver could help with that.
A specific concern was that a user can attempt to manually refactor this function:
suspend fun foo() = coroutineScope {
launch { println("1") }
launch { println("2") }
}
into
suspend fun foo() = coroutineScope {
launchThings()
}
suspend fun launchThings() {
launch { println("1") }
launch { println("2") }
}
Which will fail to compile with an error that's inscrutable to a newcomer: after all, the code is called from a coroutine scope, what's the matter?
Note: the IDE does (almost) correctly allow extracting this piece of code into
private fun CoroutineScope.launchThings(): Job {
launch {
println("ok")
}
return launch {
println("ok2")
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To further clarify the mechanism of lambda-with-receiver, It could be beneficial to additionally amend an inline comment that replicates what inlay hints can also show in the IDE, i.e.
suspend fun foo() = coroutineScope { // this: CoroutineScope
}
(if we wanted to be extra explicit, we could even make the calls to this.launch
explicit, though I think that's potentially already too-much-noise territory)
This PR rewrites the entire Coroutines basics guide to address user feedback and to align it structurally with the rest of the kotlinlang.org documentation.
It includes rewritten explanations, new examples, and more.
Related ticket: KT-56006
Related summary ticket for the entire coroutine guide rewrite efforts: KT-66329