Asynchrone streams
Notitie
Dit artikel is een functiespecificatie. De specificatie fungeert als het ontwerpdocument voor de functie. Het bevat voorgestelde specificatiewijzigingen, samen met informatie die nodig is tijdens het ontwerp en de ontwikkeling van de functie. Deze artikelen worden gepubliceerd totdat de voorgestelde specificaties zijn voltooid en opgenomen in de huidige ECMA-specificatie.
Er kunnen enkele verschillen zijn tussen de functiespecificatie en de voltooide implementatie. Deze verschillen worden vastgelegd in de relevante Language Design Meeting (LDM) notities.
Meer informatie over het proces voor het aannemen van functiespeclets in de C#-taalstandaard vindt u in het artikel over de specificaties.
Samenvatting
C# biedt ondersteuning voor iteratormethoden en asynchrone methoden, maar geen ondersteuning voor een methode die zowel een iterator als een asynchrone methode is. We moeten dit verhelpen door toe te staan dat await
in een nieuwe vorm van async
iterator wordt gebruikt, een die een IAsyncEnumerable<T>
of IAsyncEnumerator<T>
retourneert in plaats van een IEnumerable<T>
of IEnumerator<T>
, met IAsyncEnumerable<T>
verbruiksbaar in een nieuwe await foreach
. Een IAsyncDisposable
-interface wordt ook gebruikt om asynchroon opschonen in te schakelen.
Verwante discussie
Gedetailleerd ontwerp
Interfaces
IAsyncDisposable
Er is veel gesproken over IAsyncDisposable
(bijvoorbeeld https://github.com/dotnet/roslyn/issues/114) en of het een goed idee is. Het is echter een vereist concept om ondersteuning van asynchrone iterators toe te voegen. Aangezien finally
blokken await
kunnen bevatten en finally
blokken uitgevoerd moeten worden bij het verwijderen van iterators, hebben we asynchrone verwijdering nodig. Het is ook over het algemeen nuttig om bij het opschonen van resources een tijdsduur voor ogen te houden, bijvoorbeeld het sluiten van bestanden (waarbij flushes nodig zijn), het deregistreren van callbacks en een manier bieden om te weten wanneer de deregistratie is voltooid, enzovoort.
De volgende interface wordt toegevoegd aan de .NET-kernbibliotheken (bijvoorbeeld System.Private.CoreLib/ System.Runtime):
namespace System
{
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
}
Net als bij Dispose
is het aanroepen van DisposeAsync
meerdere keren acceptabel en moeten volgende aanroepen na de eerste worden behandeld als no-ops, waarbij een synchroon voltooide taak wordt geretourneerd (DisposeAsync
echter niet thread-veilig hoeven te zijn en geen gelijktijdige aanroep hoeven te ondersteunen). Bovendien kunnen typen zowel IDisposable
als IAsyncDisposable
implementeren, en als dat het geval is, is het vergelijkbaar acceptabel om Dispose
aan te roepen en vervolgens DisposeAsync
of omgekeerd, maar alleen de eerste moet zinvol zijn en volgende aanroepen van beide moeten een nop zijn. Als zodanig, als een type beide implementeert, worden consumenten aangemoedigd om één keer en slechts één keer de relevantere methode aan te roepen op basis van de context, Dispose
in synchrone contexten en DisposeAsync
in asynchrone.
(Hoe IAsyncDisposable
interageert met using
is een afzonderlijke discussie. En hoe het interageert met foreach
wordt later in dit voorstel besproken.)
Alternatieven die worden overwogen:
-
DisposeAsync
accepteren van eenCancellationToken
: hoewel het in theorie logisch is dat alles wat asynchroon kan worden geannuleerd, de verwijdering gaat over opschonen, het sluiten van dingen, het vrijmaken van resources, enz., wat over het algemeen niet iets is dat moet worden geannuleerd; opschonen is nog steeds belangrijk voor werk dat is geannuleerd. DezelfdeCancellationToken
die ervoor zorgde dat het werkelijke werk werd geannuleerd, zou doorgaans hetzelfde token zijn dat aanDisposeAsync
werd doorgegeven, waardoorDisposeAsync
waardeloos wordt omdat de annulering van het werk ertoe leidt datDisposeAsync
een no-opwordt. Als iemand wil voorkomen dat hij of zij geblokkeerd wordt bij het wachten op verwijdering, kan hij of zij vermijden te wachten op de resulterendeValueTask
, of slechts voor een bepaalde periode erop wachten. -
DisposeAsync
retourneert eenTask
: Nu er een niet-algemeenValueTask
bestaat dat kan worden samengesteld uit eenIValueTaskSource
, kanDisposeAsync
ValueTask
retourneren, waardoor een bestaand object opnieuw kan worden gebruikt als de belofte die de uiteindelijke asynchrone voltooiing vanDisposeAsync
vertegenwoordigt. Dit bespaart eenTask
toewijzing, in het geval datDisposeAsync
asynchroon wordt voltooid. -
Het configureren van
DisposeAsync
met eenbool continueOnCapturedContext
(ConfigureAwait
): Hoewel er mogelijk problemen zijn met betrekking tot de manier waarop een dergelijk concept wordt blootgesteld aanusing
,foreach
en andere taalconstructies die dit verbruiken, doet het vanuit interfaceperspectief geenawait
'ing en er is niets te configureren... consumenten van deValueTask
kunnen het verbruiken op de gewenste wijze. -
IAsyncDisposable
het overnemen vanIDisposable
: aangezien slechts één of de andere moeten worden gebruikt, is het niet zinvol om typen af te dwingen om beide te implementeren. -
IDisposableAsync
in plaats vanIAsyncDisposable
: we volgen de naamgeving dat dingen/typen een 'asynchroon iets' zijn, terwijl bewerkingen 'asynchroon' zijn, dus typen hebben 'Async' als voorvoegsel en methoden hebben 'Async' als achtervoegsel.
IAsyncEnumerable / IAsyncEnumerator
Er worden twee interfaces toegevoegd aan de kernbibliotheken van .NET:
namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
ValueTask<bool> MoveNextAsync();
T Current { get; }
}
}
Normaal verbruik (zonder extra taalfuncties) ziet er als volgt uit:
IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
Use(enumerator.Current);
}
}
finally { await enumerator.DisposeAsync(); }
Genegeerde opties overwogen:
-
Task<bool> MoveNextAsync(); T current { get; }
: het gebruik vanTask<bool>
ondersteunt het gebruik van een taakobject in de cache om synchrone, geslaagdeMoveNextAsync
aanroepen weer te geven, maar een toewijzing is nog steeds vereist voor asynchrone voltooiing. DoorValueTask<bool>
te retourneren, kunnen we het enumerator-object zelfIValueTaskSource<bool>
implementeren en worden gebruikt als backing voor deValueTask<bool>
geretourneerd doorMoveNextAsync
, waardoor op zijn beurt aanzienlijk minder overhead mogelijk is. -
ValueTask<(bool, T)> MoveNextAsync();
: Het is niet alleen moeilijker te verwerken, maar het betekent ook datT
niet langer covariant kan zijn. -
ValueTask<T?> TryMoveNextAsync();
: Niet covariant. -
Task<T?> TryMoveNextAsync();
: Niet covariant, allocaties bij elke oproep, enzovoort. -
ITask<T?> TryMoveNextAsync();
: Niet covariant, allocaties bij elke aanroep, enzovoort. -
ITask<(bool,T)> TryMoveNextAsync();
: Niet covariant, toewijzingen van geheugen bij elke aanroep, enz. -
Task<bool> TryMoveNextAsync(out T result);
: Hetout
resultaat moet worden ingesteld wanneer de bewerking synchroon wordt voltooid, niet wanneer deze asynchroon mogelijk pas veel later in de toekomst wordt afgerond, op welk moment het onmogelijk zou zijn om het resultaat te communiceren. -
IAsyncEnumerator<T>
geenIAsyncDisposable
implementeren: we kunnen ervoor kiezen om deze te scheiden. Als u dit echter doet, worden bepaalde andere gebieden van het voorstel gecompliceerd, omdat code vervolgens in staat moet zijn om te kunnen omgaan met de mogelijkheid dat een enumerator geen verwijdering biedt, waardoor het moeilijk is om helpers op basis van patronen te schrijven. Verder is het gebruikelijk dat enumerators behoefte hebben aan verwijdering (bijvoorbeeld een C#-asynchrone iterator met een laatste blok, de meeste dingen die gegevens uit een netwerkverbinding inventariseren, enzovoort), en als dat niet het geval is, is het eenvoudig om de methode puur te implementeren alspublic ValueTask DisposeAsync() => default(ValueTask);
met minimale extra overhead. - _
IAsyncEnumerator<T> GetAsyncEnumerator()
: Geen annuleringstokenparameter.
In de volgende subsectie worden alternatieven besproken die niet zijn gekozen.
Levensvatbaar alternatief:
namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator();
}
public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
ValueTask<bool> WaitForNextAsync();
T TryGetNext(out bool success);
}
}
TryGetNext
wordt gebruikt in een binnenste lus om items te gebruiken met één interfaceaanroep zolang ze synchroon beschikbaar zijn. Wanneer het volgende item niet synchroon kan worden opgehaald, retourneert het onwaar en wanneer het onwaar retourneert, moet een beller vervolgens WaitForNextAsync
aanroepen om te wachten tot het volgende item beschikbaar is of om te bepalen of er nooit een ander item zal zijn. Normaal verbruik (zonder extra taalfuncties) ziet er als volgt uit:
IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.WaitForNextAsync())
{
while (true)
{
int item = enumerator.TryGetNext(out bool success);
if (!success) break;
Use(item);
}
}
}
finally { await enumerator.DisposeAsync(); }
Het voordeel hiervan is tweevoudig, één klein en één belangrijk:
-
Secundair: hiermee kan een enumerator meerdere consumentenondersteunen. Er kunnen scenario's zijn waarin het waardevol is voor een enumerator om meerdere gelijktijdige consumenten te ondersteunen. Dat kan niet worden bereikt wanneer
MoveNextAsync
enCurrent
gescheiden zijn op een manier dat een implementatie hun gebruik niet atomisch kan maken. In tegenstelling hiermee biedt deze benadering één methodeTryGetNext
die het mogelijk maakt de enumerator voort te bewegen en het volgende item te verkrijgen, zodat de enumerator atomiciteit kan inschakelen indien gewenst. Het is echter waarschijnlijk dat dergelijke scenario's ook kunnen worden ingeschakeld door elke consument zijn eigen enumerator van een gedeelde verzameling te geven. Verder willen we niet afdwingen dat elke enumerator gelijktijdig gebruik ondersteunt, omdat hierdoor niet-triviale overhead wordt toegevoegd aan de meerderheidscase die dit niet vereist, wat betekent dat een consument van de interface in het algemeen niet op deze manier kan vertrouwen. -
Belangrijk: Prestatie. De
MoveNextAsync
/Current
aanpak vereist twee interfaceaanroepen per bewerking, terwijl het beste voorWaitForNextAsync
/TryGetNext
is dat de meeste iteraties synchroon worden voltooid, waardoor een strakke binnenlus metTryGetNext
mogelijk is, zodat we slechts één interfaceaanroep per bewerking hebben. Dit kan een meetbare impact hebben in situaties waarin de interface-aanroepen de berekening overheerst.
Er zijn echter niet-triviale nadelen, waaronder aanzienlijk verhoogde complexiteit bij het gebruik van deze handmatig en een verhoogde kans op het introduceren van bugs bij het gebruik ervan. En hoewel de prestatievoordelen in microbenchmarks worden weergegeven, geloven we niet dat ze van grote invloed zijn op het grootste deel van het echte gebruik. Als blijkt dat ze geschikt zijn, kunnen we een tweede set interfaces op een verhelderende manier introduceren.
Genegeerde opties overwogen:
-
ValueTask<bool> WaitForNextAsync(); bool TryGetNext(out T result);
:out
parameters kunnen geen covariant zijn. Er is hier ook een kleine impact (een probleem met het try-patroon in het algemeen) omdat dit patroon waarschijnlijk een schrijfbarrière tijdens runtime met zich meebrengt voor resultaten uit referentietypen.
Annulering
Er zijn verschillende manieren om annulering te ondersteunen:
-
IAsyncEnumerable<T>
/IAsyncEnumerator<T>
zijn annuleringsagnostisch:CancellationToken
komt nergens voor. Annulering wordt bereikt door deCancellationToken
logisch te integreren in de enumerable en/of enumerator op passende wijze, bijvoorbeeld bij het aanroepen van een iterator, het doorgeven van deCancellationToken
als argument aan de iteratormethode en het gebruik ervan in de implementatie van de iterator, zoals wordt gedaan met een andere parameter. -
IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken)
: U geeft eenCancellationToken
door aanGetAsyncEnumerator
en de daaropvolgendeMoveNextAsync
bewerkingen respecteren het waar mogelijk. -
IAsyncEnumerator<T>.MoveNextAsync(CancellationToken)
: U geeft eenCancellationToken
door aan elke afzonderlijkeMoveNextAsync
-aanroep. - 1 && 2: U kunt
CancellationToken
insluiten in uw enumerable/enumerator enCancellationToken
doorgeven aanGetAsyncEnumerator
. - 1 && 3: Jullie beiden voegen
CancellationToken
's in je enumerable/enumerator en gevenCancellationToken
's door aanMoveNextAsync
.
Vanuit een puur theoretisch perspectief is (5) de robuustste, omdat (a) MoveNextAsync
het accepteren van een CancellationToken
de fijnmazigste controle over wat geannuleerd wordt mogelijk maakt, en (b) CancellationToken
gewoon elk ander type is dat als een argument aan iterators kan worden doorgegeven, ingesloten in willekeurige typen, enzovoort.
Er zijn echter meerdere problemen met deze aanpak:
- Hoe komt een
CancellationToken
die aanGetAsyncEnumerator
is doorgegeven in het lichaam van de iterator terecht? We kunnen een nieuwiterator
trefwoord introduceren waarmee u toegang kunt krijgen tot deCancellationToken
die is doorgegeven aanGetEnumerator
, maar a) dat brengt veel extra mechanisme met zich mee, b) we maken het een volwaardig onderdeel van de taal, en c) het 99% geval lijkt te zijn dat dezelfde code zowel een iterator aanroept alsGetAsyncEnumerator
daarop aanroept, in dat geval kan deCancellationToken
als argument worden doorgegeven aan de methode. - Hoe komt een
CancellationToken
, doorgegeven aanMoveNextAsync
, in het lichaam van de methode? Dit is nog erger, alsof het wordt blootgesteld aan eeniterator
lokaal object, de waarde ervan kan veranderen tijdens afwachting, wat betekent dat elke code die bij het token is ingeschreven, zich voor het wachten moet uitschrijven en daarna opnieuw moet inschrijven; Het kan ook heel kostbaar zijn om zulke uitschrijf- en inschrijfprocedures in elkeMoveNextAsync
-aanroep uit te voeren, ongeacht of deze door de compiler in een iterator of door een ontwikkelaar handmatig worden geïmplementeerd. - Hoe annuleert een ontwikkelaar een
foreach
lus? Als dit wordt gedaan door eenCancellationToken
te geven aan een opsomming/opsommingselement, dan hebben we twee opties: a) we moetenforeach
over enumerators ondersteunen, wat hen tot eersteklas burgers verheft, en dan zult u moeten nadenken over een ecosysteem dat is opgebouwd rond enumerators (bijvoorbeeld LINQ-methoden), of b) we moeten deCancellationToken
sowieso insluiten in de opsomming door gebruik te maken van eenWithCancellation
-uitbreidingsmethode vanIAsyncEnumerable<T>
die het opgegeven token opslaat en dit vervolgens doorgeeft aan de verpakte opsomming'sGetAsyncEnumerator
wanneer deGetAsyncEnumerator
op de geretourneerde structuur wordt aangeroepen (terwijl dat token wordt genegeerd). Of je kunt gewoon deCancellationToken
gebruiken die je in het lichaam van de foreach hebt. - Als/wanneer querybegrippen worden ondersteund, hoe wordt de
CancellationToken
doorgegeven aanGetEnumerator
ofMoveNextAsync
en in elke clausule opgenomen? De eenvoudigste manier is simpelweg dat de clausule het vastlegt, waarna elk token dat aanGetAsyncEnumerator
/MoveNextAsync
wordt doorgegeven, genegeerd wordt.
Een eerdere versie van dit document raadde (1) aan, maar sindsdien zijn we overgestapt naar (4).
De twee belangrijkste problemen met (1):
- nl-NL: Producenten van enumerabelen die kunnen worden geannuleerd moeten enige boilerplatecode implementeren en kunnen alleen gebruikmaken van de ondersteuning van de compiler voor asynchrone iterators om een
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken)
-methode te implementeren. - het is waarschijnlijk dat veel producenten geneigd zijn om alleen een
CancellationToken
parameter toe te voegen aan hun asynchrone handtekening, waardoor consumenten het gewenste annuleringstoken niet kunnen doorgeven wanneer ze eenIAsyncEnumerable
type krijgen.
Er zijn twee belangrijke verbruiksscenario's:
-
await foreach (var i in GetData(token)) ...
waar de consument de asynchrone iterator-methode aanroept, -
await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ...
waar de consument met een gegevenIAsyncEnumerable
exemplaar omgaat.
We vinden dat een redelijk compromis om beide scenario's te ondersteunen op een manier die handig is voor zowel producenten als consumenten van asynchrone streams is om een speciaal geannoteerde parameter te gebruiken in de asynchrone iterator-methode. Voor dit doel wordt het kenmerk [EnumeratorCancellation]
gebruikt. Als u dit kenmerk op een parameter plaatst, vertelt de compiler dat als een token wordt doorgegeven aan de methode GetAsyncEnumerator
, dat token moet worden gebruikt in plaats van de waarde die oorspronkelijk is doorgegeven voor de parameter.
Overweeg IAsyncEnumerable<int> GetData([EnumeratorCancellation] CancellationToken token = default)
.
De implementeerfunctie van deze methode kan gewoon de parameter in de hoofdtekst van de methode gebruiken.
De consument kan een van de bovenstaande verbruikspatronen gebruiken:
- Als u
GetData(token)
gebruikt, wordt het token opgeslagen in de asynchrone lijst en zal het worden gebruikt bij het itereren. - als u
givenIAsyncEnumerable.WithCancellation(token)
gebruikt, vervangt het token dat is doorgegeven aanGetAsyncEnumerator
een token dat is opgeslagen in de asynchrone enumerable.
foreach
foreach
zal worden uitgebreid ter ondersteuning van IAsyncEnumerable<T>
naast de bestaande ondersteuning voor IEnumerable<T>
. En het ondersteunt het equivalent van IAsyncEnumerable<T>
als een patroon, waarbij de relevante leden openbaar worden gemaakt. Als dat niet lukt, wordt teruggevallen op het directe gebruik van de interface, om structuurgebaseerde extensies mogelijk te maken die het toewijzen vermijden; en ook om alternatieve awaitables te gebruiken als het retourtype van MoveNextAsync
en DisposeAsync
.
Syntaxis
De syntaxis gebruiken:
foreach (var i in enumerable)
C# blijft enumerable
behandelen als een synchrone opsomming, zodat zelfs als deze de relevante API's beschikbaar maakt voor asynchrone enumerables (waarbij het patroon zichtbaar wordt of de interface wordt geïmplementeerd), alleen rekening wordt gehouden met de synchrone API's.
Als u wilt afdwingen dat foreach
in plaats daarvan alleen rekening houdt met de asynchrone API's, wordt await
als volgt ingevoegd:
await foreach (var i in enumerable)
Er zou geen syntaxis worden opgegeven die ondersteuning biedt voor het gebruik van de asynchrone api's of de synchronisatie-API's; de ontwikkelaar moet kiezen op basis van de gebruikte syntaxis.
Semantiek
De compilatietijdverwerking van een await foreach
instructie bepaalt eerst het verzamelingstype, opsommingstype en iteratietype van de expressie (vergelijkbaar met https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/statements.md#1395-the-foreach-statement). Deze bepaling gaat als volgt:
- Als het type
X
van uitdrukkingdynamic
is of een arraytype, wordt een fout gegenereerd en worden er geen verdere stappen ondernomen. - Als dat niet het geval is, moet u bepalen of het type
X
een geschikteGetAsyncEnumerator
methode heeft:- Voer lidzoekactie uit op het type
X
met id-GetAsyncEnumerator
en geen typeargumenten. Als het opzoeken van het lid geen overeenkomst oplevert, een dubbelzinnigheid oplevert, of een overeenkomst oplevert die geen methodegroep is, controleer dan op een opsombare interface zoals hieronder beschreven. - Voer overbelastingsresolutie uit met behulp van de resulterende methodegroep en een lege lijst met argumenten. Als overbelastingsresolutie resulteert in geen toepasselijke methoden, resulteert in dubbelzinnigheid of resulteert in één beste methode, maar die methode is statisch of niet openbaar, controleert u op een opsommingsbare interface, zoals hieronder wordt beschreven.
- Als het retourtype
E
van de methodeGetAsyncEnumerator
geen klasse, struct of interfacetype is, wordt er een fout gegenereerd en worden er geen verdere stappen uitgevoerd. - Het opzoeken van leden wordt uitgevoerd op
E
met de identifierCurrent
zonder typeargumenten. Als het opzoeken van een lid geen overeenkomst oplevert, het resultaat een fout is, of het resultaat iets anders is dan een openbare instantie-eigenschap die lezen toestaat, wordt er een fout gegenereerd en worden er geen verdere stappen ondernomen. - Het lid opzoeken wordt uitgevoerd op
E
met de identificatorMoveNextAsync
en zonder typeargumenten. Als de opzoekactie van het lid geen overeenkomst oplevert, het resultaat een fout is, of het resultaat iets anders dan een methodegroep is, wordt er een fout gegenereerd en worden er geen verdere stappen ondernomen. - Overbelastingsresolutie wordt uitgevoerd op de methodegroep met een lege lijst met argumenten. Als overbelastingsresolutie resulteert in geen toepasselijke methoden, resulteert dit in een dubbelzinnigheid of resulteert in één beste methode, maar deze methode is statisch of niet openbaar, of het retourtype kan niet worden verwacht in
bool
, er wordt een fout gegenereerd en er worden geen verdere stappen ondernomen. - Het verzamelingstype is
X
, het enumeratortype isE
en het iteratietype is het type van de eigenschapCurrent
.
- Voer lidzoekactie uit op het type
- Controleer anders of er een enumerable interface is:
- Als er tussen alle typen
Tᵢ
waarvoor een impliciete conversie vanX
naarIAsyncEnumerable<ᵢ>
is, is er een uniek typeT
zodanig datT
niet dynamisch is en voor alle andereTᵢ
er een impliciete conversie vanIAsyncEnumerable<T>
naarIAsyncEnumerable<Tᵢ>
is, is het verzamelingstype de interfaceIAsyncEnumerable<T>
, is het enumerator-type de interfaceIAsyncEnumerator<T>
, en het iteratietype isT
. - Als er meer dan één dergelijk type
T
is, wordt er een fout gegenereerd en worden er geen verdere stappen uitgevoerd.
- Als er tussen alle typen
- Anders wordt een fout gegenereerd en worden er geen verdere stappen ondernomen.
De bovenstaande stappen, indien geslaagd, produceren ondubbelzinnig een verzamelingstype C
, opsommingstype E
en iteratietype T
.
await foreach (V v in x) «embedded_statement»
wordt vervolgens uitgebreid naar:
{
E e = ((C)(x)).GetAsyncEnumerator();
try {
while (await e.MoveNextAsync()) {
V v = (V)(T)e.Current;
«embedded_statement»
}
}
finally {
... // Dispose e
}
}
De inhoud van het finally
blok wordt samengesteld volgens de volgende stappen:
- Als het type
E
een geschikteDisposeAsync
methode heeft:- Voer lidzoekactie uit op het type
E
met id-DisposeAsync
en geen typeargumenten. Als de opzoekactie van het lid geen overeenkomst produceert of een dubbelzinnigheid produceert of een overeenkomst produceert die geen methodegroep is, controleert u of de verwijderingsinterface is zoals hieronder wordt beschreven. - Voer overbelastingsresolutie uit met behulp van de resulterende methodegroep en een lege lijst met argumenten. Als overbelastingsresolutie resulteert in geen toepasselijke methoden, resulteert in dubbelzinnigheid of resulteert in één beste methode, maar die methode is statisch of niet openbaar, controleert u op de verwijderingsinterface zoals hieronder wordt beschreven.
- Als het retourtype van de methode
DisposeAsync
niet kan worden verwacht, wordt er een fout gegenereerd en worden er geen verdere stappen ondernomen. - De
finally
component wordt uitgebreid naar het semantische equivalent van:
finally { await e.DisposeAsync(); }
- Voer lidzoekactie uit op het type
- Als er anders een impliciete conversie is van
E
naar deSystem.IAsyncDisposable
-interface, dan- Als
E
een niet-null-waardetype is, wordt definally
-component uitgebreid naar het semantische equivalent van:
finally { await ((System.IAsyncDisposable)e).DisposeAsync(); }
- Anders wordt de
finally
-component uitgebreid naar het semantische equivalent van:
Behalve dat alsfinally { System.IAsyncDisposable d = e as System.IAsyncDisposable; if (d != null) await d.DisposeAsync(); }
E
een waardetype is of een typeparameter die is geïnstantieerd naar een waardetype, de conversie vane
naarSystem.IAsyncDisposable
geen boxing mag veroorzaken.
- Als
- Anders wordt de
finally
-component uitgebreid naar een leeg blok:finally { }
ConfigureAwait
Met deze compilatie op basis van een patroon kan ConfigureAwait
worden gebruikt voor alle wachten, via een ConfigureAwait
uitbreidingsmethode:
await foreach (T item in enumerable.ConfigureAwait(false))
{
...
}
Dit is gebaseerd op typen die we ook toevoegen aan .NET, waarschijnlijk System.Threading.Tasks.Extensions.dll:
// Approximate implementation, omitting arg validation and the like
namespace System.Threading.Tasks
{
public static class AsyncEnumerableExtensions
{
public static ConfiguredAsyncEnumerable<T> ConfigureAwait<T>(this IAsyncEnumerable<T> enumerable, bool continueOnCapturedContext) =>
new ConfiguredAsyncEnumerable<T>(enumerable, continueOnCapturedContext);
public struct ConfiguredAsyncEnumerable<T>
{
private readonly IAsyncEnumerable<T> _enumerable;
private readonly bool _continueOnCapturedContext;
internal ConfiguredAsyncEnumerable(IAsyncEnumerable<T> enumerable, bool continueOnCapturedContext)
{
_enumerable = enumerable;
_continueOnCapturedContext = continueOnCapturedContext;
}
public ConfiguredAsyncEnumerator<T> GetAsyncEnumerator() =>
new ConfiguredAsyncEnumerator<T>(_enumerable.GetAsyncEnumerator(), _continueOnCapturedContext);
public struct ConfiguredAsyncEnumerator<T>
{
private readonly IAsyncEnumerator<T> _enumerator;
private readonly bool _continueOnCapturedContext;
internal ConfiguredAsyncEnumerator(IAsyncEnumerator<T> enumerator, bool continueOnCapturedContext)
{
_enumerator = enumerator;
_continueOnCapturedContext = continueOnCapturedContext;
}
public ConfiguredValueTaskAwaitable<bool> MoveNextAsync() =>
_enumerator.MoveNextAsync().ConfigureAwait(_continueOnCapturedContext);
public T Current => _enumerator.Current;
public ConfiguredValueTaskAwaitable DisposeAsync() =>
_enumerator.DisposeAsync().ConfigureAwait(_continueOnCapturedContext);
}
}
}
}
Houd er rekening mee dat met deze methode ConfigureAwait
niet kan worden gebruikt met op patronen gebaseerde enumerables, maar nogmaals is het al het geval dat de ConfigureAwait
alleen wordt weergegeven als een extensie op Task
/Task<T>
/ValueTask
/ValueTask<T>
en niet kan worden toegepast op willekeurige wachtende dingen, omdat het alleen zinvol is wanneer deze wordt toegepast op taken (het bepaalt een gedrag dat is geïmplementeerd in de vervolgondersteuning van de taak), en is dus niet zinvol bij het gebruik van een patroon waarbij de te wachten dingen mogelijk geen taken zijn. Wie wachtbare elementen retourneert, kan zijn eigen aangepaste gedrag implementeren in dergelijke geavanceerde scenario's.
(Als we een manier kunnen bedenken om een ConfigureAwait
-oplossing op bereik- of assemblyniveau uit te werken, is dit niet nodig.)
Asynchrone iterators
De taal/compiler ondersteunt het produceren én het gebruiken van IAsyncEnumerable<T>
's en IAsyncEnumerator<T>
's. Tegenwoordig ondersteunt de taal het schrijven van een iterator zoals:
static IEnumerable<int> MyIterator()
{
try
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(1000);
yield return i;
}
}
finally
{
Thread.Sleep(200);
Console.WriteLine("finally");
}
}
maar await
kan niet worden gebruikt in het lichaam van deze iterators. We voegen die ondersteuning toe.
Syntaxis
Met de huidige taalondersteuning voor iterators wordt de iterator-aard van de methode afgeleid op basis van het al dan niet bevatten van een yield
. Hetzelfde geldt voor asynchrone iterators. Dergelijke asynchrone iterators worden afgebakend en onderscheiden van synchrone iterators door async
toe te voegen aan de handtekening en moeten vervolgens ook IAsyncEnumerable<T>
of IAsyncEnumerator<T>
hebben als retourtype. Het bovenstaande voorbeeld kan bijvoorbeeld als asynchrone iterator worden geschreven:
static async IAsyncEnumerable<int> MyIterator()
{
try
{
for (int i = 0; i < 100; i++)
{
await Task.Delay(1000);
yield return i;
}
}
finally
{
await Task.Delay(200);
Console.WriteLine("finally");
}
}
Alternatieven die worden overwogen:
-
async
niet gebruiken in de handtekening: het gebruik vanasync
is waarschijnlijk technisch vereist voor de compiler, omdat deze wordt gebruikt om te bepalen ofawait
in die context geldig is. Maar zelfs als dit niet vereist is, hebben we vastgesteld datawait
alleen kan worden gebruikt in methoden die zijn gemarkeerd alsasync
, en het lijkt belangrijk om de consistentie te behouden. -
Aangepaste bouwers inschakelen voor
IAsyncEnumerable<T>
: dat is iets wat we in de toekomst kunnen bekijken, maar de machines zijn ingewikkeld en we ondersteunen dat niet voor de synchrone tegenhangers. -
Met een
iterator
trefwoord in de handtekening: Asynchrone iterators zoudenasync iterator
in de handtekening gebruiken enyield
alleen kunnen worden gebruikt inasync
methoden dieiterator
bevatten;iterator
wordt vervolgens optioneel gemaakt voor synchrone iterators. Afhankelijk van uw perspectief heeft dit het voordeel om via de handtekening van de methode heel duidelijk te maken ofyield
is toegestaan en of de methode daadwerkelijk bedoeld is om exemplaren van het typeIAsyncEnumerable<T>
te retourneren, in plaats van dat deze door de compiler gegenereerd wordt op basis van het al dan niet gebruikmaken vanyield
. Maar het verschilt van synchrone iterators, die niet en kunnen niet worden gemaakt om er een te vereisen. Bovendien vinden sommige ontwikkelaars de extra syntaxis niet leuk. Als we het helemaal opnieuw ontwerpen, zouden we dit waarschijnlijk nodig maken, maar op dit moment is er veel meer waarde om asynchrone iterators dicht bij synchronisatie-iterators te houden.
LINQ
Er zijn meer dan 200 overbelastingen van methoden in de System.Linq.Enumerable
klasse, die allemaal werken in termen van IEnumerable<T>
; sommige van deze accepteren IEnumerable<T>
, sommige daarvan produceren IEnumerable<T>
en velen doen beide. Het toevoegen van LINQ-ondersteuning voor IAsyncEnumerable<T>
zou vermoedelijk leiden tot het dupliceren van al deze overbelastingen, voor nog eens ongeveer 200. En omdat IAsyncEnumerator<T>
waarschijnlijk vaker wordt gebruikt als een zelfstandige entiteit in de asynchrone wereld dan IEnumerator<T>
zich in de synchrone wereld bevindt, kunnen we mogelijk nog eens ~200 overbelastingen nodig hebben die met IAsyncEnumerator<T>
werken. Bovendien heeft een groot aantal overbelastingen te maken met predicaten (bijvoorbeeld Where
die een Func<T, bool>
vergt), en het kan wenselijk zijn om IAsyncEnumerable<T>
-gebaseerde overbelastingen te hebben die zowel synchrone als asynchrone predicaten verwerken (bijvoorbeeld Func<T, ValueTask<bool>>
naast Func<T, bool>
). Hoewel dit niet van toepassing is op alle nu ~400 nieuwe overbelastingen, is een ruwe berekening dat het van toepassing is op de helft, wat betekent dat nog eens ~200 overbelastingen, voor een totaal van circa 600 nieuwe methoden.
Dat is een enorm groot aantal API's, met het potentieel voor nog meer wanneer uitbreidingsbibliotheken zoals Interactive Extensions (Ix) worden overwogen. Maar Ix heeft al een implementatie van veel van deze, en er lijkt geen goede reden om dat werk te dupliceren; in plaats daarvan moeten we de community helpen Ix te verbeteren en aan te bevelen voor wanneer ontwikkelaars LINQ willen gebruiken met IAsyncEnumerable<T>
.
Er is ook een probleem met de syntaxis van het begrijpen van zoekopdrachten. Door de patroongebaseerde aard van querybegrips kunnen ze 'gewoon werken' met sommige operators, bijvoorbeeld als Ix de volgende methoden biedt:
public static IAsyncEnumerable<TResult> Select<TSource, TResult>(this IAsyncEnumerable<TSource> source, Func<TSource, TResult> func);
public static IAsyncEnumerable<T> Where(this IAsyncEnumerable<T> source, Func<T, bool> func);
dan werkt deze C#-code 'gewoon':
IAsyncEnumerable<int> enumerable = ...;
IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select item * 2;
Er is echter geen syntaxis voor vragenbegrip die ondersteuning biedt voor het gebruik van await
in de clausules, dus als Ix bijvoorbeeld zou toevoegen:
public static IAsyncEnumerable<TResult> Select<TSource, TResult>(this IAsyncEnumerable<TSource> source, Func<TSource, ValueTask<TResult>> func);
dan zou dit 'gewoon werken':
IAsyncEnumerable<string> result = from url in urls
where item % 2 == 0
select SomeAsyncMethod(item);
async ValueTask<int> SomeAsyncMethod(int item)
{
await Task.Yield();
return item * 2;
}
maar er is geen manier om het te schrijven met de await
inline in de select
clausule. Als aparte inspanning, zouden we kunnen overwegen om async { ... }
-uitdrukkingen aan de taal toe te voegen. Op dat moment zouden we kunnen toestaan dat ze in querybegrippen worden gebruikt en het bovenstaande zou dan in plaats daarvan als volgt kunnen worden geschreven:
IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select async
{
await Task.Yield();
return item * 2;
};
of om await
rechtstreeks in expressies te kunnen gebruiken, bijvoorbeeld door async from
te ondersteunen. Het is echter onwaarschijnlijk dat een ontwerp hier van invloed is op de rest van de functieset op één manier of op de andere, en dit is geen bijzonder belangrijk ding om op dit moment in te investeren, dus het voorstel is om hier nu niets extra's te doen.
Integratie met andere asynchrone frameworks
Integratie met IObservable<T>
en andere asynchrone frameworks (bijvoorbeeld reactieve streams) wordt uitgevoerd op bibliotheekniveau in plaats van op taalniveau. Alle gegevens van een IAsyncEnumerator<T>
kunnen bijvoorbeeld worden gepubliceerd naar een IObserver<T>
door simpelweg await foreach
'via de enumerator en OnNext
'de gegevens aan de waarnemer toe te staan, zodat een AsObservable<T>
uitbreidingsmethode mogelijk is. Als u een IObservable<T>
in een await foreach
gebruikt, moet u de gegevens bufferen (voor het geval een ander item nog wordt gepusht terwijl het vorige item nog wordt verwerkt), maar een dergelijke push-pull-adapter kan eenvoudig worden geïmplementeerd om een IObservable<T>
met een IAsyncEnumerator<T>
te kunnen ophalen. Rx/Ix biedt al prototypen van dergelijke implementaties en bibliotheken zoals https://github.com/dotnet/corefx/tree/master/src/System.Threading.Channels bieden verschillende soorten buffergegevensstructuren. De taal hoeft in dit stadium niet te worden gebruikt.
C# feature specifications