microsoft / microsoft/vs-threading

Add group cancellation support to `AsyncLazy<T>` value factory

Open
#952 2 comments 2 reactions 1 assignee View on GitHub

@AArnott is already working on this.

Since Mar 9, 2023.

enhancement
Dominant language
C#
Stars
1k
Forks
160
Avg merge
1d 12h
Merged PRs (30d)
28

Description

Is your feature request related to a problem? Please describe.

AsyncLazy<T> currently never cancels its value factory even if all callers of GetValue or GetValueAsync have canceled their token.
While this makes for a simple value factory that never cancels and only runs at most once, it can be inefficient when all callers lost interest and the value factory no longer needs to run.

Describe the solution you'd like

Add support for a value factory that takes CancellationToken as a parameter.
When all outstanding GetValue and GetValueAsync callers cancel their own tokens, cancel the value factory.
The value factory is subject to re-invocation only after it throws OperationCanceledException as a result of a canceled token passed to it. If a new GetValueAsync caller comes along while a prior the value factory invocation is still running, that caller must await the completion of that value factory before invoking it again. If the value factory completes successfully, use (and retain) its value. If the value factory cancels, GetValueAsync may re-invoke the factory.

The invariants would be:

  1. A value factory that takes no CancellationToken would never be invoked more than once.
  2. A cancelable value factory will never be invoked concurrently with itself.
  3. A cancelable value factory will never be invoked more than once except after the prior call throws OperationCanceledException and the CancellationToken we provided them is canceled. Note in this check we do not compare the token with OperationCanceledException.CancellationToken because the implementation detail of the value factory may have combined our token with another so its identity may have changed even though it canceled ultimately in response to our token.

Consider exposing this additional functionality via a static factory method on AsyncLazy<T> instead of on a public constructor if it means we can avoid adding fields to the AsyncLazy<T> class, since this class is used a lot and each added field will add memory pressure to apps that use it, such as VS.

Describe alternatives you've considered

We considered running the value factory concurrently with itself after a prior invocation was canceled in order to expedite the result to a subsequent caller. This was rejected for a few reasons:

  1. Writing a value factory that is concurrency-safe seems like a high bar that would lead to buggy code in those that use AsyncLazy<T>.
  2. A subsequent caller that awaits the "canceled" value factory before invoking it again may in fact find that the initial invocation completed successfully rather than responding to the cancellation, in which case the fastest and most efficient thing was in fact to wait for the first invocation to complete.
  3. Running the value factory concurrently may end up with multiple values produced. If the values require disposal or the factory had side-effects, this could result in leaks or other undesirable behavior.
Additional context

This design was hashed out with @matteo-prosperi and @sealslicer.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.