-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Document structured concurrency #4433
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: develop
Are you sure you want to change the base?
Document structured concurrency #4433
Conversation
ea4a81c to
4ad4272
Compare
murfel
left a comment
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.
Let me know if it's too nitpicky. Or if you'd prefer the first round to be more high-level, and then you'd be interested in nitpicky to polish it up. Your feedback to my feedback is appreciated 🙂
| * thus enforcing the structured concurrency. See [Job] documentation for more details. | ||
| * The methods of this interface are not intended to be called directly. | ||
| * Instead, a [CoroutineScope] is passed as a receiver to the coroutine builders such as [launch] and [async], | ||
| * controlling the execution properties and lifetimes of the created 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.
lifetimes -> lifecycles
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 elaborate?
| * The [CoroutineScope.cancel] extension function shall be used when the entity that was launching coroutines | ||
| * is no longer needed. It cancels all the coroutines that might still be running on behalf of it. | ||
| * Manual implementations of this interface are not recommended. | ||
| * Instead, there are several structured ways to obtain a [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.
structured
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?
| * Some commonly used dispatchers are provided in the [Dispatchers] object. | ||
| * - [CoroutineExceptionHandler] that defines how failures of child coroutines should be reported whenever | ||
| * structured concurrency does not provide a way to propagate the failure to the parent | ||
| * (typically, because the root scope of the ancestry tree is not lexically scoped). |
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.
fyi: this is where I had to lookup what "lexically scoped" means, my understanding from the google summary is "where / in which context it is defined / written in the source code"
but this still leaves me wondering what are the specific cases this "not lexically scoped" refers to
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 managed to get the gist of "lexically" after reading this section, but I stumbled every time I saw this word.
It only gets clear after the Lexical scopes and CoroutineScope constructor sections.
Possibly define it, avoid using before these sections, or refer to those sections?
| * [coroutineScope] and [supervisorScope] functions can be used in any `suspend` function to define a scope | ||
| * lexically, ensuring that all coroutines launched in this scope have completed by the time scope-limiting | ||
| * function exits. |
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.
Split the sentence in two to simplify reading. Less ideas per sentence.
| * [coroutineScope] and [supervisorScope] functions can be used in any `suspend` function to define a scope | |
| * lexically, ensuring that all coroutines launched in this scope have completed by the time scope-limiting | |
| * function exits. | |
| * [coroutineScope] and [supervisorScope] functions can be used in any `suspend` function to define a scope | |
| * lexically. All coroutines launched in the new scope will have completed by the time the scope-limiting | |
| * function exits. |
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.
Maybe it's worth avoiding the new term "scope-limiting function". Maybe "by the time the lexical scope function exits"?
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.
Less ideas per sentence is good, but is it really okay to have less than one idea per sentence? In my view, it's better to have a longer sentence that describes a single idea instead of having two sentences with 0.5 ideas each.
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.
the new term "scope-limiting function". Maybe "by the time the lexical scope function exits"?
"The lexical scope function" is also a new term, though. What makes a function lexical-scope? Fully stated, coroutineScope is a function that limits the coroutine scope to the lexical scope of its block, yet this is a mouthful and distracts from the idea that's being conveyed. Do you have an idea of how this could be expressed clearly but also succinctly?
| * } // will only exit once all `Coroutine X finished` messages are printed | ||
| * ``` | ||
| * | ||
| * This should be the preferred way to create a scope for 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.
Move it before the example? So that there's no confusion if it refers to this section, or if it's a setup for the next section.
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.
Is it confusing in the rendered form as well? It's expected that this text will be read from kotlinlang.org or from the IDE in the popup window that appears on a mouse-over.
| * It is convenient for launching top-level coroutines that are not tied to the lifecycle of any entity, | ||
| * but it is easy to misuse it and create memory leaks or resource leaks when a coroutine actually should be tied | ||
| * to the lifecycle of some entity. |
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.
Simplify by splitting into two, less ideas per sentence.
However, it is easy to misuse...
(Let me know if you'd like me to point out other occurrences of this in my future reading bouts of this PR.)
(By the way, prompting an LLM with "rewrite this sentence" gives out the same sentence split that what I would suggest, and some stylistically better words that I wouldn't come up with myself. Although it needs some editing to put all the erased terms back in.)
| * ``` | ||
| * GlobalScope.launch(CoroutineExceptionHandler { _, e -> | ||
| * println("Error in coroutine: $e") | ||
| * }) { | ||
| * while (true) { | ||
| * println("I will be running forever, you cannot stop me!") | ||
| * delay(1.seconds) | ||
| * } | ||
| * } | ||
| * ``` |
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 feel like it's more of an example, unrelated to GlobalScope, for "if you define your custom exception handler which ignores exceptions, no one will be able to cancel you", rather than "if you misuse GlobalScope, you're setting yourself up for failure".
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 it relies on the hidden curriculum of "cancelation is implemented as an exception" which I believe hasn't been covered up to here yet.
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.
if you define your custom exception handler which ignores exceptions, no one will be able to cancel you
That's not true, though. The CoroutineExceptionHandler is here just to solidify the good practice of supplying them. Removing the CoroutineExceptionHandler does not change the point.
| * When all else fails and a custom [CoroutineScope] implementation is needed, it is recommended to use | ||
| * `by`-delegation to implement the interface: |
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.
Fails is a very specific word and brings unneeded connotations.
When no other approaches are applicable
| * } | ||
| * ``` | ||
| * | ||
| * ### `by`-delegation |
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 a bit surprised that I only learn that I should use by-delegration instead of the constructor [function] at the very end of the section. Possibly mention in the beginning that the preferred approach is the by-delegation, and the constructor is only spelled out there for clarity/demonstration?
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.
The example is using the constructor function here to obtain a non-custom implementation. The constructor function is better than by-delegation, which is in turn better than override val coroutineContext: CoroutineContext.
|
Higher-level overview first is nice, since there's no point in debating the grammar of a section that gets removed as a whole. So, the nitpicking should (initially) be limited to exactly the level above which everything's already well done. |
| * Some commonly used dispatchers are provided in the [Dispatchers] object. | ||
| * - [CoroutineExceptionHandler] that defines how failures of child coroutines should be reported whenever | ||
| * structured concurrency does not provide a way to propagate the failure to the parent | ||
| * (typically, because the root scope of the ancestry tree is not lexically scoped). |
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.
Would a specific example make it clear?
| * (typically, because the root scope of the ancestry tree is not lexically scoped). | |
| * (typically, because the root scope of the ancestry tree is not lexically scoped, | |
| that is, not created using builders like [coroutineScope] or [withContext] | |
| that return the result directly to the caller). |
| * It is convenient for launching top-level coroutines that are not tied to the lifecycle of any entity, | ||
| * but it is easy to misuse it and create memory leaks or resource leaks when a coroutine actually should be tied | ||
| * to the lifecycle of some entity. |
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.
Splitting removes the focus on the contradiction, which is the whole point. Would this work instead?
| * It is convenient for launching top-level coroutines that are not tied to the lifecycle of any entity, | |
| * but it is easy to misuse it and create memory leaks or resource leaks when a coroutine actually should be tied | |
| * to the lifecycle of some entity. | |
| * Although is convenient for launching top-level coroutines that are not tied to the lifecycle of any entity, | |
| * it is easy to misuse it and create memory leaks or resource leaks when a coroutine actually should be tied | |
| * to the lifecycle of some entity. |
| * A scope defines a group of coroutines with shared execution properties | ||
| * (represented as [CoroutineContext.Element] values) |
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.
| * A scope defines a group of coroutines with shared execution properties | |
| * (represented as [CoroutineContext.Element] values) | |
| * A scope defines a group of coroutines with shared execution properties | |
| * (for example, on which threads the coroutines execute or which thread-local variables are tracked in them) |
Would this clarify the meaning of "execution properties" here and for the rest of the kdoc?
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.
Late comment 🙈
There's nothing thread-local in a coroutine scope. CoroutineContext.Elements are an analogue, but are not thread-bound. Mentioning "thread-local" like this would create confusion with ThreadLocal in Java and equivalent in other platforms or libraries.
"are tracked in them" isn't clear to me either.
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.
What I meant to do here was provide ThreadContextElement as an example of coroutine context elements. These represent thread-local variables that are tracked in a coroutine. Does this make sense? Could you suggest a better phrasing?
142162c to
8e247be
Compare
| * | ||
| * Such a [CoroutineScope] receiver is provided for [launch], [async], and other coroutine builders, | ||
| * as well as for scoping functions like [coroutineScope], [supervisorScope], and [withContext]. | ||
| * Each of these [CoroutineScope] instances is tied to the lifecycle of the code block it runs in. |
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.
lexical
| * } | ||
| * ``` | ||
| * | ||
| * Because every coroutine has a lifecycle and a [Job], a [CoroutineScope] can be associated with 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.
- Job is a lifecycle, so "and hence a Job representing it", they are not equal
- the corollary is not clear
- it's more like: every coroutine is represented in code by CoroutineScope, hence CoroutineScope's Job represents the coroutine's lifecycle (should be at the top of the section)
| * A coroutine cannot reach the final state until all its children have reached their final states. | ||
| * See the example above. | ||
| * | ||
| * If a [CoroutineScope] is cancelled (either explicitly or because it corresponds to some coroutine that failed |
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.
can a coroutine be cancelled? or it's only "scope can be cancelled", "coroutine can fail with an exception"
| * If a coroutine fails with a non-[CancellationException] exception, | ||
| * is not a coroutine created with lexically scoped coroutine builders like [coroutineScope] or [withContext], | ||
| * *and* its parent is a normal [Job] (not a [SupervisorJob]), | ||
| * the parent fails with that exception, too (and the same logic applies recursively to the parent of the parent, etc.): |
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.
(wording) general note: too long till the punchline, move it to the front:
if a coroutine fails with ..., the parent fails with the same exception if:
- one
- two
- three
| * // the calling coroutine will *not* be cancelled, | ||
| * // even though the caller is a parent of the failed `coroutineScope`, | ||
| * // because `coroutineScope` is a lexically scoped coroutine builder, | ||
| * // which propagate their exceptions by rethrowing 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.
I didn't understand the explanation
| * | ||
| * If a coroutine fails with a non-[CancellationException] exception and cannot cancel its parent | ||
| * (because its parent is a [SupervisorJob] or there is none at all), | ||
| * the failure is reported through other channels. |
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.
through other means, since channel is a term
| * | ||
| * ``` | ||
| * scope.launch(start = CoroutineStart.ATOMIC) { | ||
| * // Do not move `NonCancellable` to the `context` argument of `launch`! |
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.
argument -> parameter
| * | ||
| * Additional context elements can be appended to the scope using the [plus][CoroutineScope.plus] operator. | ||
| * The execution properties of coroutines are defined by the [coroutineContext] property of this interface | ||
| * and are inherited by all coroutines launched in this scope. |
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.
CoroutineContext = Job + execution properties + other stuff
Draft for now, because the documentation of some other functions should be updated accordingly:
CoroutineScope.kt.supervisorScopeBuilders.common.kt:launch,async, and the other coroutine builders should document their structured concurrency behaviors.withTimeoutfamily of functions.produce.promise.futurefunction.but reviewing the changes should already be okay (I'll introduce further changes in separate commits).
cc @LouisCAD, given your real-world experience and interest in the documentation, your comments on this would be greatly appreciated!