You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Will print done immediatly.
This is different than Trio, where all tasks in a nursery (or task group) must finish before the nursery's context manager exits.
Maybe I am missing something, but otherwise I was wondering about the reasons behind this design choice.
in Effection child tasks cannot outlive their parent but their lifetime doesn't really affect their parent.
That is mostly correct, however child tasks do affect the lifetime of their parent insofar as they must exit completely before the parent can exit. The following example derived from yours above will not sleep for a full2000ms, however it will delay 500ms before exiting:
It will print "done" immediately, but it cannot exit until the finally block is fully run which includes a 500ms delay.
This is different than Trio, where all tasks in a nursery (or task group) must finish before the nursery's context manager exits.
Maybe I am missing something, but otherwise I was wondering about the reasons behind this design choice.
No, you're not missing anything. In fact, the first version of Effection was loosely based on Trio and it mimicked the same behavior regarding task lifetime. However, we found ourselves still having to spend a lot of thought cycles on explicitly managing children. In our specific case, we were spinning up several operating system processes to act as servers, bundlers, and general workers. When it came time to return from the parent computation (usually because the user hit CTRL-C), we still found ourselves having to do too much management of task cancellation by hand.
It was actually @jnicklas that had the insight that you really only begin a concurrent operation for one of two reasons: Either to produce some persistent side-effect, or else to consume its outcome directly; usually in the form of a value. In the case of the former, there is no need to keep a persistent side effect around once you return from an operation. We found that by definition, there was almost never a time that we wanted to keep things like sub resources and sub processes around, and when there was, it was elegantly handled by spawning in a parent scope that was explicitly passed in. In the cases where a task was spawned concurrently in order to consume a value, the caller would be required to explicitly depend on that value anyway with something like let result = yield* task and so we found that the actual content and scope of an operation very succinctly captured the appropriate lifetime of its concurrent children.
We experimented with appending "a child may not outlive its parent" to the original constraint "a parent is not complete until all its children are complete" and we were very happy with the result: it is very rare that you have to ever do any explicit cancellation or management of resources in Effection. Folks new to Effection are very often surprised by how aggressive it is at tearing down its children at first, but it comes to feel very clean and natural after a very short time. After all, to declare that lifetime is based on nothing more than lexical scope is the essence of structured concurrency, no? Perhaps a more philosophical way to put it is in terms of the proverbial tree in the forest: "If a concurrent operation computes, but there is no one there to listen to its value, does it exist?" our answer is no.
However if you look closely, I do believe that we have preserved, albeit in a slightly modified form, the essential aspect of Trio's guarantee with respect to the lifetimes of its children. As demonstrated in the example above, while Effection will instruct all child tasks to return when a parent task exits, it will still await the outcome of all those returns before it considers itself complete and able to report its result to any callers it might have.
Activity
cowboyd commentedon Feb 14, 2025
That is mostly correct, however child tasks do affect the lifetime of their parent insofar as they must exit completely before the parent can exit. The following example derived from yours above will not sleep for a full
2000ms, however it will delay500msbefore exiting:It will print "done" immediately, but it cannot exit until the finally block is fully run which includes a 500ms delay.
No, you're not missing anything. In fact, the first version of Effection was loosely based on Trio and it mimicked the same behavior regarding task lifetime. However, we found ourselves still having to spend a lot of thought cycles on explicitly managing children. In our specific case, we were spinning up several operating system processes to act as servers, bundlers, and general workers. When it came time to return from the parent computation (usually because the user hit CTRL-C), we still found ourselves having to do too much management of task cancellation by hand.
It was actually @jnicklas that had the insight that you really only begin a concurrent operation for one of two reasons: Either to produce some persistent side-effect, or else to consume its outcome directly; usually in the form of a value. In the case of the former, there is no need to keep a persistent side effect around once you return from an operation. We found that by definition, there was almost never a time that we wanted to keep things like sub resources and sub processes around, and when there was, it was elegantly handled by spawning in a parent scope that was explicitly passed in. In the cases where a task was spawned concurrently in order to consume a value, the caller would be required to explicitly depend on that value anyway with something like
let result = yield* taskand so we found that the actual content and scope of an operation very succinctly captured the appropriate lifetime of its concurrent children.We experimented with appending "a child may not outlive its parent" to the original constraint "a parent is not complete until all its children are complete" and we were very happy with the result: it is very rare that you have to ever do any explicit cancellation or management of resources in Effection. Folks new to Effection are very often surprised by how aggressive it is at tearing down its children at first, but it comes to feel very clean and natural after a very short time. After all, to declare that lifetime is based on nothing more than lexical scope is the essence of structured concurrency, no? Perhaps a more philosophical way to put it is in terms of the proverbial tree in the forest: "If a concurrent operation computes, but there is no one there to listen to its value, does it exist?" our answer is no.
However if you look closely, I do believe that we have preserved, albeit in a slightly modified form, the essential aspect of Trio's guarantee with respect to the lifetimes of its children. As demonstrated in the example above, while Effection will instruct all child tasks to return when a parent task exits, it will still await the outcome of all those returns before it considers itself complete and able to report its result to any callers it might have.