Design choice question #975
-
If I understand correctly, in Effection child tasks cannot outlive their parent but their lifetime doesn't really affect their parent. For instance: import { run, sleep, spawn } from "@effection/effection";
run(function* op() {
let task1 = yield* spawn(() => sleep(1000));
let task2 = yield* spawn(() => sleep(2000));
// yield* task1;
// yield* task2;
console.log('done');
}); Will print |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 5 replies
-
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 import { run, sleep, spawn } from "@effection/effection";
run(function* op() {
yield* spawn(function*() {
try {
yield* sleep(2000);
} finally {
yield* sleep(500);
}
});
console.log('done');
}); 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 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. |
Beta Was this translation helpful? Give feedback.
-
Hmmm... This seems like a bug, but I'm not able to reproduce it. What version of Effection are you using, and which operating system are you using? Also, what happens if you use |
Beta Was this translation helpful? Give feedback.
-
Oh I was on the |
Beta Was this translation helpful? Give feedback.
-
I hear you. We just found that there was less cognitive load with the reverse as the default. It might be due to the use-cases we were applying to Effection, but again, we found that if the type of a task was In other words, the following was very rare yield* all(my_tasks) And more often would resemble either: let my_results = yield* all(my_tasks); or return yield* all(my_tasks); Here is an example from the It is a subtle difference with Trio, but the core invariant of structured concurrency is still present in that any child that has been entered must exit fully before its parent can return. I'm curious what you think of it as you develop more and more in both. |
Beta Was this translation helpful? Give feedback.
-
I don't know what to think yet :) |
Beta Was this translation helpful? Give feedback.
I hear you. We just found that there was less cognitive load with the reverse as the default. It might be due to the use-cases we were applying to Effection, but again, we found that if the type of a task was
Task<void>
and it was being run purely for its persistent side-effects, then the vast majority of the time, we would want to cancel it at the end. To not would be the edge case. However, if t…