[Proposal] Ref and ref-like parameters of async methods #9997
Unanswered
IS4Code
asked this question in
Language Ideas
Replies: 2 comments 5 replies
-
|
I will champion this. |
Beta Was this translation helpful? Give feedback.
0 replies
-
|
This would be huge for |
Beta Was this translation helpful? Give feedback.
5 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
This is a follow-up to #1331, extending the relaxation of
refandref structvariables inasyncmethods to method parameters as well, with the same overall semantics. Under this proposal,ref/in/outparameters or parameters ofref-like types shall be usable inasyncmethods (possibly only for compatible builders;Task/ValueTaskat minimum), and shall have the same behaviour as locals, i.e. be available up to anawaitboundary (and require reassignment afterwards to be usable again).This proposal does not cover iterator methods, since their code always executes lazily.
Motivation
Consider a general message-receiving component:
Such an interface is designed with these goals in mind:
ValueTaskso it can optimize synchronous paths (indeed many receivers might not need to implement it asynchronously at all).Messageis astructbecause its construction might happen over a local, and the same local can be reused for multiple sequential messages, reducing GC overhead (even using an object pool complicates reasoning about the lifetime of messages ‒ using a reference here protects against unwanted escaping).Messageis a potentially bulky object with many fields gradually added as the application grows, it is passed by-reference, because not all receivers need to access all its fields (but the interface must stay the same, so the method can't request these fields explicitly).In this context, the receivers are supposed to process the messages as they arrive, sometimes pass the references around into nested receivers, while a single dispatcher constructs the messages in-place (in an allocation-less style). Most receivers aren't asynchronous, and most messages do not need to persist past the
ReceiveMessageAsynccall, justifying this design. Sincemessageis immutable, all receivers are always able to read all relevant fields up front using the reference, and do any processing later.The issue appears immediately when trying to implement this method via
asynceven when there is noawait:Such a pattern is somewhat common to capture any exceptions into the returned
ValueTask, as most callers expect. Even in this case, with 100% synchronous execution (and hencemessagebeing available the whole time), the parameter is currently not allowed.The usefulness of this pattern is not restricted to
await-less code, of course:Here all relevant data is extracted from the message first and utilized later, even after
await.Currently, one needs to go through a local function to implement such a method conveniently:
However this can still be prone to issues regarding exception-wrapping and other potentially erroneous semantics, such as using
usingin the outer method, and it is far from perfect ‒ sincemessagemust not be accessed withinInnerat all, everything must be stored up front.Benefits
All in all, this proposal would bring in:
Inner(in regards tousing).Design
The overall design is similar to that of local variables:
asyncmethods can have parameters that areref/out/inorref-like.await.awaitboundary, they are considered definitely unassigned. Consistently with byref locals, byref parameters can be reassigned to be usable again, e.g.param = ref expr;.That is, the semantics of such parameters within the implementation of an
asyncmethod are functionally identical to that of locals defined immediately at the start of the method.outUnlike other cases,
outparameters pose requirements on the implementation itself. In the case of anoutparameter, its value must be definitely assigned by the point of the firstawait, in accordance with what the caller expects from calling the method withoutawait.Reference escaping and scoping
Like in normal methods, these relaxed rules make it possible for references from parameters (or references to
thisor its members) to escape viareforoutparameters. Such anasyncmethod should have the exact same characteristics as a non-asyncmethod sharing the initial portion of the implementation up until the firstawaitboundary, i.e. what can be achieved using theInnerfunction as above, again matching what the caller expects.Examples
Span processing
This method takes an input span of an arbitrary length, identifies the length of the thing serialized within
data, parses it, adjusts the span accordingly so it points after the thing indata, and handles the thing off to additional processing.This mechanism ensures the extent of the thing within
datais known even before the final processing, it also correctly wraps any exceptions during parsing in the resultingValueTask, and it ensures cleanup ofthingat end of the processing.Possible extensions
int*[] paramare allowed anyway). Such a thing could potentially be changed to a warning.ref/ref-like locals cannot be used in a local function or anything else that could generate a closure. In case this restriction is lifted, it could also apply toasyncfunctions referencing outside variables or parameters, due to the similarity between a closure and arefparameter.Beta Was this translation helpful? Give feedback.
All reactions