Dela via


Asynkrona strömmar

Obs

Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.

Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader dokumenteras i de relevanta anteckningarna från Language Design Meeting (LDM) .

Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.

Sammanfattning

C# har stöd för iteratormetoder och asynkrona metoder, men inget stöd för en metod som är både en iterator och en asynkron metod. Vi bör åtgärda detta genom att tillåta att await används i en ny form av async iterator, en som returnerar en IAsyncEnumerable<T> eller IAsyncEnumerator<T> i stället för en IEnumerable<T> eller IEnumerator<T>, med IAsyncEnumerable<T> som kan användas i en ny await foreach. Ett IAsyncDisposable-gränssnitt används också för att aktivera asynkron rensning.

Detaljerad design

Gränssnitt

IAsyncDisposable

Det har varit mycket diskussion om IAsyncDisposable (t.ex. https://github.com/dotnet/roslyn/issues/114) och om det är en bra idé. Det är dock ett obligatoriskt koncept att lägga till till stöd för asynkrona iteratorer. Eftersom finally block kan innehålla awaitoch eftersom finally block måste köras som en del av disponering av iteratorer behöver vi asynkron borttagning. Det är också bara allmänt användbart när det tar lång tid att rensa resurser, t.ex. att stänga filer (kräver tömningar), avregistrera återanrop och ge ett sätt att veta när avregistreringen har slutförts osv.

Följande gränssnitt läggs till i kärnbiblioteken för .NET (t.ex. System.Private.CoreLib/System.Runtime):

namespace System
{
    public interface IAsyncDisposable
    {
        ValueTask DisposeAsync();
    }
}

Precis som med Disposeär det acceptabelt att anropa DisposeAsync flera gånger, och efterföljande anrop efter den första ska behandlas som no-ops, vilket returnerar en synkront slutförd lyckad uppgift (DisposeAsync behöver dock inte vara trådsäker och behöver inte ha stöd för samtidig anrop). Dessutom kan typer implementera både IDisposable och IAsyncDisposable, och om de gör det är det på samma sätt acceptabelt att anropa Dispose och sedan DisposeAsync eller vice versa, men bara det första ska ha effekt och efterföljande anrop av någon av dem bör vara en nop. Därmed, om en typ implementerar båda, uppmanas konsumenterna att endast anropa den mer relevanta metoden en gång och endast en gång, baserat på kontexten, Dispose i synkrona kontexter och DisposeAsync i asynkrona.

(Hur IAsyncDisposable interagerar med using är en separat diskussion. Och täckning av hur det interagerar med foreach hanteras senare i det här förslaget.)

Alternativ som övervägs:

  • DisposeAsync acceptera en CancellationToken: även om det i teorin är vettigt att något asynkront kan avbrytas, handlar resursfrisättning om rensning, stänga saker, frigöra resurser, etc., vilket i allmänhet inte är något som bör avbrytas; rensning är fortfarande viktigt för arbete som har avbrutits. Samma CancellationToken som gjorde att det faktiska arbetet avbröts skulle vanligtvis vara samma token som skickas till DisposeAsync, vilket gör DisposeAsync värdelös eftersom inställandet av arbetet skulle göra DisposeAsync till en no-op. Om någon vill undvika att blockeras i väntan på bortskaffande kan de undvika att vänta på den resulterande ValueTask, eller bara vänta på det under en viss tidsperiod.
  • DisposeAsync returnera en Task: Nu när det finns en icke-generisk ValueTask som kan konstrueras från en IValueTaskSource, innebär att returnera ValueTask från DisposeAsync att ett befintligt objekt kan återanvändas som ett löfte som representerar den asynkrona fullföljanden av DisposeAsync, vilket sparar en Task-allokering i de fall där DisposeAsync slutförs asynkront.
  • Konfigurera DisposeAsync med en bool continueOnCapturedContext (ConfigureAwait): Även om det kan finnas problem som rör hur ett sådant begrepp exponeras för using, foreachoch andra språkkonstruktioner som förbrukar detta, gör det faktiskt inte något await"ing och det finns inget att konfigurera ... konsumenter av ValueTask kan konsumera det hur de vill.
  • IAsyncDisposable ärver IDisposable: Eftersom endast den ena eller den andra ska användas är det inte meningsfullt att tvinga typer att implementera båda.
  • IDisposableAsync i stället för IAsyncDisposable: Vi har följt namngivningen att saker/typer är "asynkrona" medan åtgärder "görs asynkront", så typerna har "Async" som prefix och metoderna har "Async" som suffix.

IAsyncEnumerable/IAsyncEnumerator

Två gränssnitt läggs till i kärnbiblioteken för .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; }
    }
}

Typisk förbrukning (utan ytterligare språkfunktioner) skulle se ut så här:

IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();
try
{
    while (await enumerator.MoveNextAsync())
    {
        Use(enumerator.Current);
    }
}
finally { await enumerator.DisposeAsync(); }

Övervägda alternativ som ignorerats:

  • Task<bool> MoveNextAsync(); T current { get; }: Om du använder Task<bool> kan du använda ett cachelagrat aktivitetsobjekt för att representera synkrona, lyckade MoveNextAsync-anrop, men en allokering krävs fortfarande för asynkront slutförande. Genom att returnera ValueTask<bool>gör vi det möjligt för uppräkningsobjektet att implementera IValueTaskSource<bool> och användas som stöd för ValueTask<bool> som returneras från MoveNextAsync, vilket i sin tur möjliggör avsevärt minskade omkostnader.
  • ValueTask<(bool, T)> MoveNextAsync();: Det är inte bara svårare att använda, men det innebär att T inte längre kan vara covariant.
  • ValueTask<T?> TryMoveNextAsync();: Inte covariant.
  • Task<T?> TryMoveNextAsync();: Inte samvariant, allokering vid varje anrop, osv.
  • ITask<T?> TryMoveNextAsync();: Icke-kovariant, allokeringar för varje anrop osv.
  • ITask<(bool,T)> TryMoveNextAsync();: Inte kovariant, minnesallokeringar vid varje anrop, etc.
  • Task<bool> TryMoveNextAsync(out T result);: Resultatet i out måste anges när operationen returnerar synkront, inte när den asynkront slutförs potentiellt någon gång i framtiden, vid vilken tidpunkt det inte skulle finnas något sätt att kommunicera resultatet.
  • IAsyncEnumerator<T> implementerar inte IAsyncDisposable: Vi kan välja att separera dessa. Men att göra så komplicerar vissa andra områden i förslaget, eftersom koden då måste kunna hantera möjligheten att en uppräknare inte stödjer frisläppning, vilket gör det svårt att skriva mönsterbaserade hjälpfunktioner. Dessutom är det vanligt att uppräknare ofta behöver använda sig av avslutande åtgärder (t.ex. alla C#-asynkrona iteratorer som har ett finally-block, de flesta saker som räknar upp data från en nätverksanslutning osv.), och om en inte har det behovet är det enkelt att implementera metoden enbart som public ValueTask DisposeAsync() => default(ValueTask); med minimal extra omkostnad.
  • _ IAsyncEnumerator<T> GetAsyncEnumerator(): Ingen parameter för annulleringstoken.

I följande underavsnitt beskrivs alternativ som inte har valts.

Genomförbart alternativ:

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 används i en inre loop för att använda objekt med ett enda gränssnittsanrop så länge de är tillgängliga synkront. När nästa objekt inte kan hämtas synkront returneras falskt, och varje gång det returnerar falskt, måste en anropare anropa WaitForNextAsync antingen vänta tills nästa objekt är tillgängligt eller för att fastställa att det aldrig kommer att finnas något annat objekt. Typisk förbrukning (utan ytterligare språkfunktioner) skulle se ut så här:

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(); }

Fördelen med detta är tvåfaldig: en mindre och en större.

  • Minor: Tillåter att en uppräknare stöder flera konsumenter. Det kan finnas scenarier där det är värdefullt för en uppräknare att stödja flera samtidiga konsumenter. Det kan inte uppnås när MoveNextAsync och Current är separata så att en implementering inte kan göra deras användning atomisk. Det här tillvägagångssättet innehåller däremot en enda metod TryGetNext som stöder att driva uppräknaren framåt och hämta nästa objekt, så att uppräknaren kan aktivera atomaritet om så önskas. Det är dock troligt att sådana scenarier också kan aktiveras genom att ge varje konsument sin egen uppräkning från en delad uppräkning. Dessutom vill vi inte framtvinga att varje uppräknare stöder samtidig användning, eftersom det skulle lägga till icke-triviala omkostnader i majoritetsfallet som inte kräver det, vilket innebär att en konsument av gränssnittet i allmänhet inte kunde förlita sig på detta på något sätt.
  • Major: Prestanda. Den MoveNextAsync/Current metoden kräver två gränssnittsanrop per åtgärd, medan det bästa fallet för WaitForNextAsync/TryGetNext är att de flesta iterationer slutförs synkront, vilket möjliggör en nära inre loop med TryGetNext, så att vi bara har ett gränssnittsanrop per åtgärd. Detta kan ha en mätbar inverkan i situationer där gränssnittsanropen dominerar beräkningen.

Det finns dock icke-triviala nackdelar, inklusive avsevärt ökad komplexitet när du använder dessa manuellt, och en ökad chans att introducera buggar när du använder dem. Och även om prestandafördelarna visas i mikrobenchmarker tror vi inte att de kommer att påverka den stora majoriteten av den verkliga användningen. Om det visar sig att de är det, kan vi introducera en andra uppsättning gränssnitt på ett dynamiskt sätt.

Övervägda alternativ som ignorerats:

  • ValueTask<bool> WaitForNextAsync(); bool TryGetNext(out T result);: out parametrar kan inte vara kovarianta. Det finns också en liten påverkan här (ett problem med try-mönstret i allmänhet) att detta sannolikt medför en skrivbarriär vid körtid för resultat av referenstyp.

Avbeställning

Det finns flera möjliga metoder för att stödja annullering:

  1. IAsyncEnumerable<T> / IAsyncEnumerator<T> är annulleringsoberoende: CancellationToken förekommer inte någonstans. Annullering uppnås genom att logiskt integrera CancellationToken till den uppräkningsbara instansen och/eller uppräknaren på det sätt som är lämpligt, t.ex. när du anropar en iterator, skickar CancellationToken som ett argument till iteratormetoden och använder det i iteratorns kropp, som med andra parametrar.
  2. IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken): Du skickar en CancellationToken till GetAsyncEnumerator, och efterföljande MoveNextAsync åtgärder respekteras så gott det går.
  3. IAsyncEnumerator<T>.MoveNextAsync(CancellationToken): Du skickar en CancellationToken till varje enskilt MoveNextAsync anrop.
  4. 1 && 2: Du bäddar in CancellationTokeni din enumerator och skickar CancellationTokentill GetAsyncEnumerator.
  5. 1 && 3: Du bäddar båda in CancellationTokeni din uppräkningsbara/uppräkning och skickar CancellationTokens till MoveNextAsync.

Ur ett rent teoretiskt perspektiv är (5) den mest robusta, eftersom (a) MoveNextAsync att acceptera en CancellationToken möjliggör den mest detaljerade kontrollen över vad som annulleras, och (b) CancellationToken är bara någon annan typ som kan passeras som ett argument till iteratorer, inbäddas i godtyckliga typer osv.

Det finns dock flera problem med den metoden:

  • Hur kan en CancellationToken som skickas till GetAsyncEnumerator komma in i kroppen av iteratorn? Vi kan exponera ett nytt iterator-nyckelord som du kan använda för att få åtkomst till CancellationToken som skickats till GetEnumerator, men a) det är en hel del ytterligare mekanik, b) vi gör det till en förstklassig funktion, och c) 99%-fallet verkar vara samma kod både för att anropa en iterator och för att anropa GetAsyncEnumerator på den, i vilket fall den bara kan skicka CancellationToken som ett argument till metoden.
  • Hur kommer en CancellationToken som skickas till MoveNextAsync in i metodens kropp? Detta är ännu värre. Om det exponerades från ett lokalt iterator-objekt, kan dess värde ändras vid väntande punkter, vilket innebär att all kod som registrerats med token skulle behöva avregistrera sig från den innan dessa punkter och sedan registrera sig igen efteråt. Det kan också vara ganska kostsamt att behöva utföra sådan registrering och avregistrering i varje MoveNextAsync-anrop, oavsett om det implementeras av kompilatorn i en iterator eller av en utvecklare manuellt.
  • Hur avbryter en utvecklare en foreach-loop? Om det görs genom att ge en CancellationToken till en uppräkningsbar/uppräknare, måste vi antingen a) stödja foreachöver uppräknare, vilket upphöjer dem till förstklassiga medborgare, och nu måste vi börja tänka på ett ekosystem som byggs upp kring uppräknare (t.ex. LINQ-metoder) eller b) vi behöver ändå bädda in CancellationToken i den uppräkningsbara genom att ha någon WithCancellation-utökningsmetod från IAsyncEnumerable<T> som skulle lagra den givna token och sedan skicka den till den omslutna uppräkningens GetAsyncEnumerator när GetAsyncEnumerator på den returnerade strukturen anropas (ignorera den token). Eller så kan du bara använda CancellationToken du har i huvuddelen av foreach-loopen.
  • Om/när frågekonstruktioner stöds, hur skulle CancellationToken överföras till varje sats hos GetEnumerator eller MoveNextAsync? Det enklaste sättet är helt enkelt att satsen samlar in den, då den token som skickas till GetAsyncEnumerator/MoveNextAsync ignoreras.

En tidigare version av det här dokumentet rekommenderade(1), men vi bytte sedan till(4).

De två största problemen med (1):

  • Producenten av avbrytbara uppräkningar måste implementera lite standardkod och kan bara utnyttja kompilatorns stöd för asynkrona iteratorer för att implementera en IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken)-metod.
  • Det är troligt att många producenter skulle frestas att bara lägga till en CancellationToken parameter i sin asynkrona signatur i stället, vilket hindrar konsumenterna från att skicka den annulleringstoken de vill ha när de får en IAsyncEnumerable typ.

Det finns två huvudsakliga förbrukningsscenarier:

  1. await foreach (var i in GetData(token)) ... där konsumenten anropar metoden async-iterator,
  2. await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ... där konsumenten hanterar ett givet IAsyncEnumerable-exemplar.

Vi anser att en rimlig kompromiss för att stödja båda scenarierna på ett sätt som är praktiskt för både producenter och konsumenter av asynkrona strömmar är att använda en särskilt kommenterad parameter i metoden async-iterator. Attributet [EnumeratorCancellation] används för detta ändamål. Om du placerar det här attributet på en parameter meddelar kompilatorn att om en token skickas till metoden GetAsyncEnumerator ska den token användas i stället för det värde som ursprungligen skickades för parametern.

Överväg IAsyncEnumerable<int> GetData([EnumeratorCancellation] CancellationToken token = default). Implementeraren av den här metoden kan helt enkelt använda parametern i metodtexten. Konsumenten kan använda förbrukningsmönstren ovan:

  1. Om du använder GetData(token)sparas token i async-enumerable och används i iterationen.
  2. Om du använder givenIAsyncEnumerable.WithCancellation(token)ersätter token som skickas till GetAsyncEnumerator alla token som sparats i async-enumerable.

foreach

foreach utökas för att stödja IAsyncEnumerable<T> utöver dess befintliga stöd för IEnumerable<T>. Och det kommer att stödja motsvarigheten till IAsyncEnumerable<T> som ett mönster om de relevanta medlemmarna är offentliga. Om de inte är det kommer det i stället att direkt använda gränssnittet. Detta möjliggör struct-baserade utvidgningar som undviker allokering samt användningen av alternativa awaitables som returtyp för MoveNextAsync och DisposeAsync.

Syntax

Med hjälp av syntaxen:

foreach (var i in enumerable)

C# fortsätter att behandla enumerable som en synkron uppräkningsbar, så att även om den exponerar relevanta API:er för asynkrona uppräkningar (exponerar mönstret eller implementerar gränssnittet), kommer det bara att överväga synkrona API:er.

Om du vill tvinga foreach att i stället bara överväga asynkrona API:er infogas await på följande sätt:

await foreach (var i in enumerable)

Ingen syntax skulle anges som stöder användning av asynkrona API:er eller synkroniserings-API:er. utvecklaren måste välja baserat på den syntax som används.

Semantik

Kompileringstidsbearbetningen av en await foreach-instruktion avgör först samlingstyp, uppräkningstyp och iterationstyp av uttrycket (ungefär som https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/statements.md#1395-the-foreach-statement). Den här bedömningen fortsätter på följande sätt:

  • Om typen X av uttryck är dynamic eller en matristyp genereras ett fel och inga ytterligare steg vidtas.
  • I annat fall avgör du om typen X har en lämplig GetAsyncEnumerator metod:
    • Utför medlemssökning på typen X med identifieraren GetAsyncEnumerator och inga typargument. Om medlemssökningen inte ger någon matchning, eller om den skapar en tvetydighet eller skapar en matchning som inte är en metodgrupp, kontrollerar du om det finns ett uppräkningsbart gränssnitt enligt beskrivningen nedan.
    • Utför överbelastningsmatchning med hjälp av den resulterande metodgruppen och en tom argumentlista. Om överbelastningsmatchning inte resulterar i några tillämpliga metoder, resulterar i en tvetydighet eller resulterar i en enda bästa metod, men den metoden antingen är statisk eller inte offentlig, kontrollerar du om det finns ett uppräkningsbart gränssnitt enligt beskrivningen nedan.
    • Om returtypen E av metoden GetAsyncEnumerator inte är en klass-, struct- eller gränssnittstyp genereras ett fel och inga ytterligare steg vidtas.
    • Medlemssökning utförs på E med identifieraren Current och inga typargument. Om medlemssökningen inte ger någon matchning är resultatet ett fel eller resultatet är något annat än en offentlig instansegenskap som tillåter läsning, ett fel skapas och inga ytterligare åtgärder vidtas.
    • Medlemssökning utförs på E med identifieraren MoveNextAsync och inga typargument. Om medlemssökningen inte ger någon matchning är resultatet ett fel, eller resultatet är något annat än en metodgrupp, ett fel genereras och inga ytterligare åtgärder vidtas.
    • Överbelastningsupplösning utförs på metodgruppen med en tom argumentlista. Om överbelastningsmatchning inte resulterar i några tillämpliga metoder resulterar det i en tvetydighet eller resulterar i en enda bästa metod, men den metoden är antingen statisk eller inte offentlig, eller så är dess returtyp inte väntande i bool, genereras ett fel och inga ytterligare åtgärder vidtas.
    • Samlingstypen är X, uppräkningstypen är Eoch iterationstypen är typen för egenskapen Current.
  • Annars kontrollerar du om det finns ett uppräkningsbart gränssnitt:
    • Om bland alla typerna Tᵢ, för vilka det finns en implicit omvandling från X till IAsyncEnumerable<ᵢ>, det finns en unik typ T så att T inte är dynamisk och för alla andra Tᵢ finns det en implicit omvandling från IAsyncEnumerable<T> till IAsyncEnumerable<Tᵢ>, då är samlingstypen gränssnittet IAsyncEnumerable<T>, uppräkningstypen är gränssnittet IAsyncEnumerator<T>och iterationstypen är T.
    • Om det finns fler än en sådan typ Tgenereras annars ett fel och inga ytterligare åtgärder vidtas.
  • Annars genereras ett fel och inga ytterligare åtgärder vidtas.

Ovanstående steg, om det lyckas, skapar entydigt en samlingstyp C, uppräkningstyp E och iterationstyp T.

await foreach (V v in x) «embedded_statement»

expanderas sedan till:

{
    E e = ((C)(x)).GetAsyncEnumerator();
    try {
        while (await e.MoveNextAsync()) {
            V v = (V)(T)e.Current;
            «embedded_statement»
        }
    }
    finally {
        ... // Dispose e
    }
}

Kroppen i finally-blocket byggs upp enligt följande steg:

  • Om typen E har en lämplig DisposeAsync metod:
    • Utför medlemssökning på typen E med identifieraren DisposeAsync och inga typargument. Om medlemssökningen inte ger någon matchning, eller om den skapar en tvetydighet eller genererar en matchning som inte är en metodgrupp, kontrollerar du om det finns ett gränssnitt för bortskaffande enligt beskrivningen nedan.
    • Utför överbelastningsmatchning med hjälp av den resulterande metodgruppen och en tom argumentlista. Om överbelastningsmatchning inte resulterar i några tillämpliga metoder, resulterar i en tvetydighet eller resulterar i en enda bästa metod, men den metoden antingen är statisk eller inte offentlig, kontrollerar du om det finns ett gränssnitt för bortskaffande enligt beskrivningen nedan.
    • Om returtypen för metoden DisposeAsync inte kan väntas, genereras ett fel och inga ytterligare åtgärder vidtas.
    • Satsen finally utökas till den semantiska motsvarigheten till:
      finally {
          await e.DisposeAsync();
      }
    
  • Annars, om det finns en implicit konvertering från E till System.IAsyncDisposable-gränssnittet,
    • Om E är en icke-nullbar värdetyp expanderas finally-satsen till den semantiska motsvarigheten till:
      finally {
          await ((System.IAsyncDisposable)e).DisposeAsync();
      }
    
    • Annars utökas finally-satsen till den semantiska motsvarigheten till:
      finally {
          System.IAsyncDisposable d = e as System.IAsyncDisposable;
          if (d != null) await d.DisposeAsync();
      }
      
      förutom att om E är en värdetyp, eller en typparameter som instansieras till en värdetyp, ska konverteringen av e till System.IAsyncDisposable inte orsaka boxning.
  • Annars expanderas finally-satsen till ett tomt block:
    finally {
    }
    

ConfigureAwait

Den här mönsterbaserade kompilering gör att ConfigureAwait kan användas på alla inväntningar via en ConfigureAwait-tilläggsmetod:

await foreach (T item in enumerable.ConfigureAwait(false))
{
   ...
}

Detta kommer också att baseras på typer som vi lägger till i .NET, troligtvis till 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);
            }
        }
    }
}

Observera att den här metoden inte gör att ConfigureAwait kan användas med mönsterbaserade uppräkningar, men det är redan så att ConfigureAwait endast exponeras som ett tillägg på Task/Task<T>/ValueTask/ValueTask<T> och inte kan tillämpas på godtyckliga väntande saker, eftersom det bara är meningsfullt när det tillämpas på uppgifter (det styr ett beteende som implementeras i uppgiftens fortsättningsstöd). och därför inte är meningsfullt när du använder ett mönster där de väntande sakerna kanske inte är uppgifter. Alla som returnerar väntande saker kan tillhandahålla sitt eget anpassade beteende i sådana avancerade scenarier.

Om vi kan komma på något sätt att stödja en ConfigureAwait-lösning på räckvidds- eller sammansättningsnivå, så kommer detta inte vara nödvändigt.

Asynkrona iteratorer

Språket/kompilatorn kommer att stödja produktion av IAsyncEnumerable<T>och IAsyncEnumerator<T>, utöver att hantera dem. I dag har språket stöd för att skriva en iterator som:

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");
    }
}

men await kan inte användas i dessa iteratorer. Vi kommer att lägga till det stödet.

Syntax

Det befintliga språkstödet för iteratorer härleder metodens iteratortyp baserat på om den innehåller några yields. Detsamma gäller för asynkrona iteratorer. Sådana asynkrona iteratorer avgränsas och särskiljs från synkrona iteratorer genom att async läggs till i signaturen och måste sedan ha antingen IAsyncEnumerable<T> eller IAsyncEnumerator<T> som returtyp. Exemplet ovan kan till exempel skrivas som en asynkron iterator på följande sätt:

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");
    }
}

Alternativ som övervägs:

  • Att inte använda async i signaturen: Kompilatorn behöver förmodligen använda async tekniskt, eftersom den använder den för att avgöra om await är giltig i den kontexten. Men även om det inte krävs har vi fastställt att await endast kan användas i metoder som har markerats som async, och det verkar viktigt att behålla konsekvensen.
  • Aktivera anpassade byggare för IAsyncEnumerable<T>: Det kan vi titta på för framtiden, men maskinerna är komplicerade och vi stöder inte det för synkrona motsvarigheter.
  • Att ha ett iterator nyckelord i signaturen: Async-iteratorer skulle använda async iterator i signaturen, och yield kunde bara användas i async metoder som inkluderade iterator; iterator blir sedan valfritt för synkrona iteratorer. Beroende på ditt perspektiv har detta fördelen att göra det mycket tydligt genom signaturen för metoden om yield tillåts och om metoden faktiskt är avsedd att returnera instanser av typen IAsyncEnumerable<T> snarare än kompilatorns tillverkning en baserat på om koden använder yield eller inte. Men det skiljer sig från synkrona iteratorer, som inte kräver och inte heller kan göras för att kräva sådan funktionalitet. Dessutom gillar vissa utvecklare inte den extra syntaxen. Om vi designade den från grunden skulle vi förmodligen göra detta obligatoriskt, men just nu finns det mycket mer värde för att hålla asynkrona iteratorer nära synkroniserings iteratorer.

LINQ

Det finns över ~200 överlagringar av metoder i klassen System.Linq.Enumerable, som alla fungerar när det gäller IEnumerable<T>; vissa av dessa accepterar IEnumerable<T>, några av dem producerar IEnumerable<T>, och många gör båda. Att lägga till LINQ-stöd för IAsyncEnumerable<T> skulle sannolikt innebära duplicering av alla dessa överlagda metoder, vilket skulle innebära ytterligare cirka 200. Och eftersom IAsyncEnumerator<T> sannolikt är vanligare som en fristående entitet i den asynkrona världen än IEnumerator<T> är i den synkrona världen, kan vi potentiellt behöva ytterligare ~200 överlagringar som fungerar med IAsyncEnumerator<T>. Dessutom hanterar ett stort antal överlagringar predikat (t.ex. Where som tar en Func<T, bool>), och det kan vara önskvärt att ha IAsyncEnumerable<T>-baserade överlagringar som hanterar både synkrona och asynkrona predikat (t.ex. Func<T, ValueTask<bool>> utöver Func<T, bool>). Även om detta inte gäller för alla nu ~400 nya överlagringar, är en grov beräkning att det skulle vara tillämpligt på hälften, vilket innebär ytterligare ~200 överlagringar, för totalt ~600 nya metoder.

Det är ett häpnadsväckande antal API:er, med potential för ännu fler när tilläggsbibliotek som Interactive Extensions (Ix) beaktas. Men Ix har redan en implementering av många av dessa, och det verkar inte finnas någon stor anledning att duplicera det arbetet; Vi bör i stället hjälpa communityn att förbättra Ix och rekommendera det för när utvecklare vill använda LINQ med IAsyncEnumerable<T>.

Det finns också problem med frågeförståelsesyntax. Den mönsterbaserade typen av frågeförståelse skulle göra det möjligt för dem att "bara arbeta" med vissa operatorer, t.ex. om Ix tillhandahåller följande metoder:

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);

då kommer den här C#-koden att "bara fungera":

IAsyncEnumerable<int> enumerable = ...;
IAsyncEnumerable<int> result = from item in enumerable
                               where item % 2 == 0
                               select item * 2;

Det finns dock ingen syntax för frågeförståelse som stöder användning av await i satserna, så om Ix läggs till, till exempel:

public static IAsyncEnumerable<TResult> Select<TSource, TResult>(this IAsyncEnumerable<TSource> source, Func<TSource, ValueTask<TResult>> func);

då skulle detta "bara fungera":

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;
}

men det skulle inte finnas något sätt att skriva det med await infogad i select-satsen. Som en separat ansträngning kan vi titta på att lägga till async { ... } uttryck i språket, då vi kan tillåta att de används i frågeförståelse och ovanstående kan i stället skrivas som:

IAsyncEnumerable<int> result = from item in enumerable
                               where item % 2 == 0
                               select async
                               {
                                   await Task.Yield();
                                   return item * 2;
                               };

eller för att möjliggöra att await används direkt i uttryck, till exempel genom att stödja async from. Det är dock osannolikt att en design här skulle påverka resten av funktionen på ett eller annat sätt, och det här är inte en särskilt värdefull sak att investera i just nu, så förslaget är att inte göra något ytterligare här just nu.

Integrering med andra asynkrona ramverk

Integrering med IObservable<T> och andra asynkrona ramverk (t.ex. reaktiva strömmar) skulle göras på biblioteksnivå i stället för på språknivå. Till exempel kan alla data från en IAsyncEnumerator<T> publiceras till en IObserver<T> helt enkelt genom att await foreachöver uppräknaren och OnNextdatan till observatören, så en AsObservable<T> extensionsmetod är möjlig. Användning av en IObservable<T> i en await foreach kräver buffring av data (om ett annat objekt trycks medan det föregående objektet fortfarande håller på att bearbetas), men en sådan push-pull-adapter kan enkelt implementeras så att en IObservable<T> kan hämtas med en IAsyncEnumerator<T>från. Rx/Ix tillhandahåller redan prototyper av sådana implementeringar samt bibliotek som https://github.com/dotnet/corefx/tree/master/src/System.Threading.Channels tillhandahåller olika typer av buffringsdatastrukturer. Språket behöver inte vara inblandat i det här skedet.