Delen via


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.

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 awaitkunnen 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 Disposeis 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 IAsyncDisposableimplementeren, 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 een CancellationToken: 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. Dezelfde CancellationToken die ervoor zorgde dat het werkelijke werk werd geannuleerd, zou doorgaans hetzelfde token zijn dat aan DisposeAsyncwerd doorgegeven, waardoor DisposeAsync waardeloos wordt omdat de annulering van het werk ertoe leidt dat DisposeAsync 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 resulterende ValueTask, of slechts voor een bepaalde periode erop wachten.
  • DisposeAsync retourneert een Task: Nu er een niet-algemeen ValueTask bestaat dat kan worden samengesteld uit een IValueTaskSource, kan DisposeAsyncValueTask retourneren, waardoor een bestaand object opnieuw kan worden gebruikt als de belofte die de uiteindelijke asynchrone voltooiing van DisposeAsyncvertegenwoordigt. Dit bespaart een Task toewijzing, in het geval dat DisposeAsync asynchroon wordt voltooid.
  • Het configureren van DisposeAsync met een bool continueOnCapturedContext (ConfigureAwait): Hoewel er mogelijk problemen zijn met betrekking tot de manier waarop een dergelijk concept wordt blootgesteld aan using, foreachen andere taalconstructies die dit verbruiken, doet het vanuit interfaceperspectief geen await'ing en er is niets te configureren... consumenten van de ValueTask kunnen het verbruiken op de gewenste wijze.
  • IAsyncDisposable het overnemen van IDisposable: 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 van IAsyncDisposable: 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 van Task<bool> ondersteunt het gebruik van een taakobject in de cache om synchrone, geslaagde MoveNextAsync aanroepen weer te geven, maar een toewijzing is nog steeds vereist voor asynchrone voltooiing. Door ValueTask<bool>te retourneren, kunnen we het enumerator-object zelf IValueTaskSource<bool> implementeren en worden gebruikt als backing voor de ValueTask<bool> geretourneerd door MoveNextAsync, waardoor op zijn beurt aanzienlijk minder overhead mogelijk is.
  • ValueTask<(bool, T)> MoveNextAsync();: Het is niet alleen moeilijker te verwerken, maar het betekent ook dat T 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);: Het out 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> geen IAsyncDisposableimplementeren: 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 als public 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 en Current gescheiden zijn op een manier dat een implementatie hun gebruik niet atomisch kan maken. In tegenstelling hiermee biedt deze benadering één methode TryGetNext 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 voor WaitForNextAsync/TryGetNext is dat de meeste iteraties synchroon worden voltooid, waardoor een strakke binnenlus met TryGetNextmogelijk 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:

  1. IAsyncEnumerable<T> / IAsyncEnumerator<T> zijn annuleringsagnostisch: CancellationToken komt nergens voor. Annulering wordt bereikt door de CancellationToken logisch te integreren in de enumerable en/of enumerator op passende wijze, bijvoorbeeld bij het aanroepen van een iterator, het doorgeven van de CancellationToken als argument aan de iteratormethode en het gebruik ervan in de implementatie van de iterator, zoals wordt gedaan met een andere parameter.
  2. IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken): U geeft een CancellationToken door aan GetAsyncEnumeratoren de daaropvolgende MoveNextAsync bewerkingen respecteren het waar mogelijk.
  3. IAsyncEnumerator<T>.MoveNextAsync(CancellationToken): U geeft een CancellationToken door aan elke afzonderlijke MoveNextAsync-aanroep.
  4. 1 && 2: U kunt CancellationTokeninsluiten in uw enumerable/enumerator en CancellationTokendoorgeven aan GetAsyncEnumerator.
  5. 1 && 3: Jullie beiden voegen CancellationToken's in je enumerable/enumerator en geven CancellationToken's door aan MoveNextAsync.

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 aan GetAsyncEnumerator is doorgegeven in het lichaam van de iterator terecht? We kunnen een nieuw iterator trefwoord introduceren waarmee u toegang kunt krijgen tot de CancellationToken die is doorgegeven aan GetEnumerator, 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 als GetAsyncEnumerator daarop aanroept, in dat geval kan de CancellationToken als argument worden doorgegeven aan de methode.
  • Hoe komt een CancellationToken, doorgegeven aan MoveNextAsync, in het lichaam van de methode? Dit is nog erger, alsof het wordt blootgesteld aan een iterator 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 elke MoveNextAsync-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 een CancellationToken te geven aan een opsomming/opsommingselement, dan hebben we twee opties: a) we moeten foreachover 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 de CancellationToken sowieso insluiten in de opsomming door gebruik te maken van een WithCancellation-uitbreidingsmethode van IAsyncEnumerable<T> die het opgegeven token opslaat en dit vervolgens doorgeeft aan de verpakte opsomming's GetAsyncEnumerator wanneer de GetAsyncEnumerator op de geretourneerde structuur wordt aangeroepen (terwijl dat token wordt genegeerd). Of je kunt gewoon de CancellationToken gebruiken die je in het lichaam van de foreach hebt.
  • Als/wanneer querybegrippen worden ondersteund, hoe wordt de CancellationToken doorgegeven aan GetEnumerator of MoveNextAsync en in elke clausule opgenomen? De eenvoudigste manier is simpelweg dat de clausule het vastlegt, waarna elk token dat aan GetAsyncEnumerator/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 een IAsyncEnumerable type krijgen.

Er zijn twee belangrijke verbruiksscenario's:

  1. await foreach (var i in GetData(token)) ... waar de consument de asynchrone iterator-methode aanroept,
  2. await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ... waar de consument met een gegeven IAsyncEnumerable 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:

  1. Als u GetData(token)gebruikt, wordt het token opgeslagen in de asynchrone lijst en zal het worden gebruikt bij het itereren.
  2. als u givenIAsyncEnumerable.WithCancellation(token)gebruikt, vervangt het token dat is doorgegeven aan GetAsyncEnumerator 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 geschikte GetAsyncEnumerator 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 methode GetAsyncEnumerator 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 identifier Current 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 identificator MoveNextAsync 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 is Een het iteratietype is het type van de eigenschap Current.
  • Controleer anders of er een enumerable interface is:
    • Als er tussen alle typen Tᵢ waarvoor een impliciete conversie van X naar IAsyncEnumerable<ᵢ>is, is er een uniek type T zodanig dat T niet dynamisch is en voor alle andere Tᵢ er een impliciete conversie van IAsyncEnumerable<T> naar IAsyncEnumerable<Tᵢ>is, is het verzamelingstype de interface IAsyncEnumerable<T>, is het enumerator-type de interface IAsyncEnumerator<T>, en het iteratietype is T.
    • Als er meer dan één dergelijk type Tis, wordt er een fout gegenereerd en worden er geen verdere stappen uitgevoerd.
  • 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 geschikte DisposeAsync 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();
      }
    
  • Als er anders een impliciete conversie is van E naar de System.IAsyncDisposable-interface, dan
    • Als E een niet-null-waardetype is, wordt de finally-component uitgebreid naar het semantische equivalent van:
      finally {
          await ((System.IAsyncDisposable)e).DisposeAsync();
      }
    
    • Anders wordt de finally-component uitgebreid naar het semantische equivalent van:
      finally {
          System.IAsyncDisposable d = e as System.IAsyncDisposable;
          if (d != null) await d.DisposeAsync();
      }
      
      Behalve dat als E een waardetype is of een typeparameter die is geïnstantieerd naar een waardetype, de conversie van e naar System.IAsyncDisposable geen boxing mag veroorzaken.
  • 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 van async is waarschijnlijk technisch vereist voor de compiler, omdat deze wordt gebruikt om te bepalen of await in die context geldig is. Maar zelfs als dit niet vereist is, hebben we vastgesteld dat await alleen kan worden gebruikt in methoden die zijn gemarkeerd als async, 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 zouden async iterator in de handtekening gebruiken en yield alleen kunnen worden gebruikt in async methoden die iteratorbevatten; 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 of yield is toegestaan en of de methode daadwerkelijk bedoeld is om exemplaren van het type IAsyncEnumerable<T> te retourneren, in plaats van dat deze door de compiler gegenereerd wordt op basis van het al dan niet gebruikmaken van yield. 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 fromte 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.