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.
Relaterad diskussion
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 await
och 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 enCancellationToken
: ä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. SammaCancellationToken
som gjorde att det faktiska arbetet avbröts skulle vanligtvis vara samma token som skickas tillDisposeAsync
, vilket görDisposeAsync
värdelös eftersom inställandet av arbetet skulle göraDisposeAsync
till en no-op. Om någon vill undvika att blockeras i väntan på bortskaffande kan de undvika att vänta på den resulterandeValueTask
, eller bara vänta på det under en viss tidsperiod. -
DisposeAsync
returnera enTask
: Nu när det finns en icke-generiskValueTask
som kan konstrueras från enIValueTaskSource
, innebär att returneraValueTask
frånDisposeAsync
att ett befintligt objekt kan återanvändas som ett löfte som representerar den asynkrona fullföljanden avDisposeAsync
, vilket sparar enTask
-allokering i de fall därDisposeAsync
slutförs asynkront. -
Konfigurera
DisposeAsync
med enbool continueOnCapturedContext
(ConfigureAwait
): Även om det kan finnas problem som rör hur ett sådant begrepp exponeras förusing
,foreach
och andra språkkonstruktioner som förbrukar detta, gör det faktiskt inte någotawait
"ing och det finns inget att konfigurera ... konsumenter avValueTask
kan konsumera det hur de vill. -
IAsyncDisposable
ärverIDisposable
: 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örIAsyncDisposable
: 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änderTask<bool>
kan du använda ett cachelagrat aktivitetsobjekt för att representera synkrona, lyckadeMoveNextAsync
-anrop, men en allokering krävs fortfarande för asynkront slutförande. Genom att returneraValueTask<bool>
gör vi det möjligt för uppräkningsobjektet att implementeraIValueTaskSource<bool>
och användas som stöd förValueTask<bool>
som returneras frånMoveNextAsync
, 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 attT
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 iout
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 inteIAsyncDisposable
: 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 sompublic 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
ochCurrent
ä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 metodTryGetNext
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örWaitForNextAsync
/TryGetNext
är att de flesta iterationer slutförs synkront, vilket möjliggör en nära inre loop medTryGetNext
, 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:
-
IAsyncEnumerable<T>
/IAsyncEnumerator<T>
är annulleringsoberoende:CancellationToken
förekommer inte någonstans. Annullering uppnås genom att logiskt integreraCancellationToken
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, skickarCancellationToken
som ett argument till iteratormetoden och använder det i iteratorns kropp, som med andra parametrar. -
IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken)
: Du skickar enCancellationToken
tillGetAsyncEnumerator
, och efterföljandeMoveNextAsync
åtgärder respekteras så gott det går. -
IAsyncEnumerator<T>.MoveNextAsync(CancellationToken)
: Du skickar enCancellationToken
till varje enskiltMoveNextAsync
anrop. - 1 && 2: Du bäddar in
CancellationToken
i din enumerator och skickarCancellationToken
tillGetAsyncEnumerator
. - 1 && 3: Du bäddar båda in
CancellationToken
i din uppräkningsbara/uppräkning och skickarCancellationToken
s tillMoveNextAsync
.
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 tillGetAsyncEnumerator
komma in i kroppen av iteratorn? Vi kan exponera ett nyttiterator
-nyckelord som du kan använda för att få åtkomst tillCancellationToken
som skickats tillGetEnumerator
, 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 anropaGetAsyncEnumerator
på den, i vilket fall den bara kan skickaCancellationToken
som ett argument till metoden. - Hur kommer en
CancellationToken
som skickas tillMoveNextAsync
in i metodens kropp? Detta är ännu värre. Om det exponerades från ett lokaltiterator
-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 varjeMoveNextAsync
-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 enCancellationToken
till en uppräkningsbar/uppräknare, måste vi antingen a) stödjaforeach
ö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 inCancellationToken
i den uppräkningsbara genom att ha någonWithCancellation
-utökningsmetod frånIAsyncEnumerable<T>
som skulle lagra den givna token och sedan skicka den till den omslutna uppräkningensGetAsyncEnumerator
närGetAsyncEnumerator
på den returnerade strukturen anropas (ignorera den token). Eller så kan du bara användaCancellationToken
du har i huvuddelen av foreach-loopen. - Om/när frågekonstruktioner stöds, hur skulle
CancellationToken
överföras till varje sats hosGetEnumerator
ellerMoveNextAsync
? Det enklaste sättet är helt enkelt att satsen samlar in den, då den token som skickas tillGetAsyncEnumerator
/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 enIAsyncEnumerable
typ.
Det finns två huvudsakliga förbrukningsscenarier:
-
await foreach (var i in GetData(token)) ...
där konsumenten anropar metoden async-iterator, -
await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ...
där konsumenten hanterar ett givetIAsyncEnumerable
-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:
- Om du använder
GetData(token)
sparas token i async-enumerable och används i iterationen. - Om du använder
givenIAsyncEnumerable.WithCancellation(token)
ersätter token som skickas tillGetAsyncEnumerator
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 ärdynamic
eller en matristyp genereras ett fel och inga ytterligare steg vidtas. - I annat fall avgör du om typen
X
har en lämpligGetAsyncEnumerator
metod:- Utför medlemssökning på typen
X
med identifierarenGetAsyncEnumerator
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 metodenGetAsyncEnumerator
inte är en klass-, struct- eller gränssnittstyp genereras ett fel och inga ytterligare steg vidtas. - Medlemssökning utförs på
E
med identifierarenCurrent
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 identifierarenMoveNextAsync
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 ärE
och iterationstypen är typen för egenskapenCurrent
.
- Utför medlemssökning på typen
- 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ånX
tillIAsyncEnumerable<ᵢ>
, det finns en unik typT
så attT
inte är dynamisk och för alla andraTᵢ
finns det en implicit omvandling frånIAsyncEnumerable<T>
tillIAsyncEnumerable<Tᵢ>
, då är samlingstypen gränssnittetIAsyncEnumerable<T>
, uppräkningstypen är gränssnittetIAsyncEnumerator<T>
och iterationstypen ärT
. - Om det finns fler än en sådan typ
T
genereras annars ett fel och inga ytterligare åtgärder vidtas.
- Om bland alla typerna
- 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ämpligDisposeAsync
metod:- Utför medlemssökning på typen
E
med identifierarenDisposeAsync
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(); }
- Utför medlemssökning på typen
- Annars, om det finns en implicit konvertering från
E
tillSystem.IAsyncDisposable
-gränssnittet,- Om
E
är en icke-nullbar värdetyp expanderasfinally
-satsen till den semantiska motsvarigheten till:
finally { await ((System.IAsyncDisposable)e).DisposeAsync(); }
- Annars utökas
finally
-satsen till den semantiska motsvarigheten till:
förutom att omfinally { System.IAsyncDisposable d = e as System.IAsyncDisposable; if (d != null) await d.DisposeAsync(); }
E
är en värdetyp, eller en typparameter som instansieras till en värdetyp, ska konverteringen ave
tillSystem.IAsyncDisposable
inte orsaka boxning.
- Om
- 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 yield
s. 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ändaasync
tekniskt, eftersom den använder den för att avgöra omawait
är giltig i den kontexten. Men även om det inte krävs har vi fastställt attawait
endast kan användas i metoder som har markerats somasync
, 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ändaasync iterator
i signaturen, ochyield
kunde bara användas iasync
metoder som inkluderadeiterator
;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 omyield
tillåts och om metoden faktiskt är avsedd att returnera instanser av typenIAsyncEnumerable<T>
snarare än kompilatorns tillverkning en baserat på om koden använderyield
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 OnNext
datan 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.
C# feature specifications