Sequential semantics of async DisposeResources
#3
Comments
After rereading README, I see that this order can be helpful then some resources should be disposed in a custom order. However, in this case, it also could be helpful an alternative container where disposing can be done in parallel. |
In this case, I believe the core API should promote the more reliable case and match the behavior of Whether or not async resources can be safely disposed in parallel is an optimization decision that only the user can make. In an effort to limit the scope of the proposal, we could consider a follow-on proposal to allow for parallel cleanup, but I don't think it meets the bar for the MVP. A follow-on proposal or library could, for example, define a const serial = new AsyncDisposableStack();
serial.defer(async () => console.log(44));
const parallel = serial.use(new ParallelAsyncDisposableStack());
parallel.defer(async () => console.log(43));
parallel.defer(async () => {
await new Promise(resolve => setTimeout(resolve, 100));
console.log(42);
});
serial.defer(async () => {
await new Promise(resolve => setTimeout(resolve, 100));
console.log(41);
});
await serial.disposeAsync();
// waits 100ms
// prints: 41
// prints: 43
// waits 100ms
// prints: 42
// prints: 44 |
I think that there can be added an option, for example, new AsyncDisposableStack({ parallel: true }); |
I've been thinking about some related challenges in efficiently acquiring and disposing of resources that do and do not have inter-dependencies. Basically, in performance-sensitive code, the sequential disposal can be in conflict with performance objectives if the serialization of promises isn't necessary. This can easily be the case in acquiring and then disposing of acquired resources. At first, I was thinking that a user-space solution might be able to address this through something like the following. {
// I acknowledge that this example is a bit of nonsense. The idea is that there might be a
// mix of resources having inter-dependencies and those without.
// The `acquireWithDependencies` function is a user-space utility to attempt to acquire
// resources with maximum concurrency where waterfall behaviour might otherwise be unavoidable.
await using stack = acquireWithDependencies({
job: [async () => takeJob()],
jobPrerequisites: ['job', async (job) => lockPrerequisiteJobs(job.id)],
machine: [async () => acquireMachineForJob()],
});
const { job, jobPrerequisites, machine } = stack.acquired;
// Do stuff with the acquired resources
// ...
// The `machine` resource is disposed in parallel to the `jobPrerequisites` resource. The
// `job` will only be freed once the `jobPrerequisites` have been freed but won't wait for
// `machine`. The block will only complete once all have been disposed.
} While I can intuit that the above can be built using the new language feature, I wonder if the primitives are sufficient to avoid the sorts of RAII pitfalls @rbuckton described for the sync proposal. One area where this becomes murky, IMHO is if the acquisition of |
That looks strange - why are some resources that can be disposed immediately should wait for something if it can be done in parallel? In case of errors, the tree can be built in the end. However, yes, here is
stack
in the name...The text was updated successfully, but these errors were encountered: