异步流Async Streams
- [x] 建议[x] Proposed
- [x] 原型[x] Prototype
- [] 实现[ ] Implementation
- [] 规范[ ] Specification
总结Summary
C # 支持迭代器方法和异步方法,但不支持同时作为迭代器和异步方法的方法。C# has support for iterator methods and async methods, but no support for a method that is both an iterator and an async method. 我们应该通过允许 await
使用新的迭代器形式来纠正这种情况 async
,它将返回 IAsyncEnumerable<T>
或 IAsyncEnumerator<T>
而不是 IEnumerable<T>
或 IEnumerator<T>
, IAsyncEnumerable<T>
在新的中使用 await foreach
。We should rectify this by allowing for await
to be used in a new form of async
iterator, one that returns an IAsyncEnumerable<T>
or IAsyncEnumerator<T>
rather than an IEnumerable<T>
or IEnumerator<T>
, with IAsyncEnumerable<T>
consumable in a new await foreach
. IAsyncDisposable
接口还用于启用异步清理。An IAsyncDisposable
interface is also used to enable asynchronous cleanup.
相关讨论Related discussion
详细设计Detailed design
接口Interfaces
IAsyncDisposableIAsyncDisposable
对 ((例如)进行了很多讨论 IAsyncDisposable
https://github.com/dotnet/roslyn/issues/114) ,这是一个好主意。There has been much discussion of IAsyncDisposable
(e.g. https://github.com/dotnet/roslyn/issues/114) and whether it's a good idea. 不过,它是添加异步迭代器支持所需的概念。However, it's a required concept to add in support of async iterators. 由于 finally
块可能包含 await
s,而且由于 finally
块需要在处理迭代器的过程中运行,因此需要异步处理。Since finally
blocks may contain await
s, and since finally
blocks need to be run as part of disposing of iterators, we need async disposal. 通常情况下,任何时候清理资源所需的时间可能会很有用,例如关闭文件 (需要刷新) ,取消注册回调并提供一种方法来了解注销完成的时间等。It's also just generally useful any time cleaning up of resources might take any period of time, e.g. closing files (requiring flushes), deregistering callbacks and providing a way to know when deregistration has completed, etc.
以下接口将添加到核心 .NET 库中 (例如 System.private.corelib/) :The following interface is added to the core .NET libraries (e.g. System.Private.CoreLib / System.Runtime):
namespace System
{
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
}
与一样 Dispose
, DisposeAsync
多次调用都是可接受的,并且第一次调用之后的调用应被视为 nops,返回同步完成的任务 (DisposeAsync
不需要线程安全的,并且无需支持) 的并发调用。As with Dispose
, invoking DisposeAsync
multiple times is acceptable, and subsequent invocations after the first should be treated as nops, returning a synchronously completed successful task (DisposeAsync
need not be thread-safe, though, and need not support concurrent invocation). 此外,类型可以实现 IDisposable
和 IAsyncDisposable
,并且如果它们这样做,则可以像调用一样接受, Dispose
DisposeAsync
反之亦然,但只有第一个应该是有意义的,后续调用都应该是 nop。Further, types may implement both IDisposable
and IAsyncDisposable
, and if they do, it's similarly acceptable to invoke Dispose
and then DisposeAsync
or vice versa, but only the first should be meaningful and subsequent invocations of either should be a nop. 因此,如果类型确实实现了这两种类型,则鼓励使用者调用一次,并且仅在 Dispose
同步上下文中和异步的上下文中调用一次相关的方法 DisposeAsync
。As such, if a type does implement both, consumers are encouraged to call once and only once the more relevant method based on the context, Dispose
in synchronous contexts and DisposeAsync
in asynchronous ones.
(,我将讨论如何 IAsyncDisposable
与进行交互 using
以进行单独讨论。(I'm leaving discussion of how IAsyncDisposable
interacts with using
to a separate discussion. foreach
稍后将在此建议中处理与之交互的方式。 ) And coverage of how it interacts with foreach
is handled later in this proposal.)
考虑的替代方案:Alternatives considered:
- 接受:尽管在理论上可以取消任何异步操作,但处置是关于清除、关闭、free'ing 资源等,这通常不是应该取消的内容; 对于取消的工作,清除仍非常重要。
DisposeAsync
CancellationToken
DisposeAsync
accepting aCancellationToken
: while in theory it makes sense that anything async can be canceled, disposal is about cleanup, closing things out, free'ing resources, etc., which is generally not something that should be canceled; cleanup is still important for work that's canceled.CancellationToken
导致实际工作被取消的同一标记通常是传递给的同一标记DisposeAsync
,这是DisposeAsync
因为工作取消将导致DisposeAsync
nop 毫无意义。The sameCancellationToken
that caused the actual work to be canceled would typically be the same token passed toDisposeAsync
, makingDisposeAsync
worthless because cancellation of the work would causeDisposeAsync
to be a nop. 如果有人想要避免被阻止的等待处置,则可以避免等待结果ValueTask
,或只等待一段时间。If someone wants to avoid being blocked waiting for disposal, they can avoid waiting on the resultingValueTask
, or wait on it only for some period of time. - 返回:既然存在非泛型并且可以从中进行构造,则从返回会
DisposeAsync
允许将现有对象作为表示的最终异步完成的承诺,从而在异步完成时保存分配。Task
ValueTask
IValueTaskSource
ValueTask
DisposeAsync
DisposeAsync
Task
DisposeAsync
DisposeAsync
returning aTask
: Now that a non-genericValueTask
exists and can be constructed from anIValueTaskSource
, returningValueTask
fromDisposeAsync
allows an existing object to be reused as the promise representing the eventual async completion ofDisposeAsync
, saving aTask
allocation in the case whereDisposeAsync
completes asynchronously. DisposeAsync
使用bool continueOnCapturedContext
(ConfigureAwait
) 进行配置:尽管可能存在与此类概念的公开方式相关的问题,using
foreach
以及使用此概念的其他语言构造,但从接口角度来看,实际上不会执行任何操作,也不会对其进行任何await
配置 .。。的使用者ValueTask
可以使用它。ConfiguringDisposeAsync
with abool continueOnCapturedContext
(ConfigureAwait
): While there may be issues related to how such a concept is exposed tousing
,foreach
, and other language constructs that consume this, from an interface perspective it's not actually doing anyawait
'ing and there's nothing to configure... consumers of theValueTask
can consume it however they wish.IAsyncDisposable
继承IDisposable
:由于仅应使用其中的一个或另一个,因此强制类型实现两者并无意义。IAsyncDisposable
inheritingIDisposable
: Since only one or the other should be used, it doesn't make sense to force types to implement both.- 而不是:我们一直在执行命名操作,其中,事物的类型为 "异步内容",而操作是 "异步" 的,因此,类型将 "Async" 作为前缀,方法将 "Async" 作为后缀。
IDisposableAsync
IAsyncDisposable
IDisposableAsync
instead ofIAsyncDisposable
: We've been following the naming that things/types are an "async something" whereas operations are "done async", so types have "Async" as a prefix and methods have "Async" as a suffix.
IAsyncEnumerable/IAsyncEnumeratorIAsyncEnumerable / IAsyncEnumerator
向核心 .NET 库添加了两个接口:Two interfaces are added to the core .NET libraries:
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; }
}
}
典型的使用 (没有其他语言功能) 将如下所示:Typical consumption (without additional language features) would look like:
IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
Use(enumerator.Current);
}
}
finally { await enumerator.DisposeAsync(); }
丢弃的选项:Discarded options considered:
Task<bool> MoveNextAsync(); T current { get; }
:使用Task<bool>
将支持使用缓存的任务对象来表示同步、成功的MoveNextAsync
调用,但异步完成仍需要分配。Task<bool> MoveNextAsync(); T current { get; }
: UsingTask<bool>
would support using a cached task object to represent synchronous, successfulMoveNextAsync
calls, but an allocation would still be required for asynchronous completion. 通过返回ValueTask<bool>
,我们允许枚举器对象自身实现IValueTaskSource<bool>
并用作从返回的的后备ValueTask<bool>
MoveNextAsync
,这反过来允许大大降低开销。By returningValueTask<bool>
, we enable the enumerator object to itself implementIValueTaskSource<bool>
and be used as the backing for theValueTask<bool>
returned fromMoveNextAsync
, which in turn allows for significantly reduced overheads.ValueTask<(bool, T)> MoveNextAsync();
:更难使用,但这意味着它T
不再是协变的。ValueTask<(bool, T)> MoveNextAsync();
: It's not only harder to consume, but it means thatT
can no longer be covariant.ValueTask<T?> TryMoveNextAsync();
:不是协变的。ValueTask<T?> TryMoveNextAsync();
: Not covariant.Task<T?> TryMoveNextAsync();
:不是协变的,每个调用的分配等。Task<T?> TryMoveNextAsync();
: Not covariant, allocations on every call, etc.ITask<T?> TryMoveNextAsync();
:不是协变的,每个调用的分配等。ITask<T?> TryMoveNextAsync();
: Not covariant, allocations on every call, etc.ITask<(bool,T)> TryMoveNextAsync();
:不是协变的,每个调用的分配等。ITask<(bool,T)> TryMoveNextAsync();
: Not covariant, allocations on every call, etc.Task<bool> TryMoveNextAsync(out T result);
:out
当操作以同步方式返回时,需要设置结果,而不是在未来的某个时间点异步完成任务,此时无法传达结果。Task<bool> TryMoveNextAsync(out T result);
: Theout
result would need to be set when the operation returns synchronously, not when it asynchronously completes the task potentially sometime long in the future, at which point there'd be no way to communicate the result.IAsyncEnumerator<T>
未实现IAsyncDisposable
:我们可以选择将它们分隔开来。IAsyncEnumerator<T>
not implementingIAsyncDisposable
: We could choose to separate these. 不过,这样做会使建议的其他某些区域变得更加复杂,因为代码必须能够处理枚举器不提供释放的可能性,这使得编写基于模式的帮助器变得困难。However, doing so complicates certain other areas of the proposal, as code must then be able to deal with the possibility that an enumerator doesn't provide disposal, which makes it difficult to write pattern-based helpers. 此外,枚举器还需要释放 (例如,具有 finally 块的任何 c # 异步迭代器、从网络连接中枚举数据的大多数事件,等等。 ) ,如果不是这样,则可以轻松实现完全相同的方法public ValueTask DisposeAsync() => default(ValueTask);
。Further, it will be common for enumerators to have a need for disposal (e.g. any C# async iterator that has a finally block, most things enumerating data from a network connection, etc.), and if one doesn't, it is simple to implement the method purely aspublic ValueTask DisposeAsync() => default(ValueTask);
with minimal additional overhead.- _
IAsyncEnumerator<T> GetAsyncEnumerator()
:无取消标记参数。_IAsyncEnumerator<T> GetAsyncEnumerator()
: No cancellation token parameter.
可行的替代方案:Viable alternative:
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
在内部循环中用于使用具有单个接口调用的项,前提是它们可以同步使用。TryGetNext
is used in an inner loop to consume items with a single interface call as long as they're available synchronously. 当无法同步检索下一项时,它将返回 false,并且任何时间如果返回 false,则调用方必须随后调用 WaitForNextAsync
以等待下一项可用或确定将永远不会有其他项。When the next item can't be retrieved synchronously, it returns false, and any time it returns false, a caller must subsequently invoke WaitForNextAsync
to either wait for the next item to be available or to determine that there will never be another item. 典型的使用 (没有其他语言功能) 将如下所示:Typical consumption (without additional language features) would look like:
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(); }
这样做的优点是两折:The advantage of this is two-fold, one minor and one major:
- 小调:允许枚举器支持多个使用者。Minor: Allows for an enumerator to support multiple consumers. 在某些情况下,可以使用枚举器来支持多个并发使用者。There may be scenarios where it's valuable for an enumerator to support multiple concurrent consumers. 当
MoveNextAsync
和是独立的,使Current
实现无法使其使用原子时,无法实现。That can't be achieved whenMoveNextAsync
andCurrent
are separate such that an implementation can't make their usage atomic. 与此相反,此方法提供了一个方法TryGetNext
,该方法支持向前推送枚举器和获取下一项,因此,如果需要,枚举器可以启用原子性。In contrast, this approach provides a single methodTryGetNext
that supports pushing the enumerator forward and getting the next item, so the enumerator can enable atomicity if desired. 但是,这种情况很可能是通过为每个使用者提供来自共享可枚举的使用者自己的枚举器来实现的。However, it's likely that such scenarios could also be enabled by giving each consumer its own enumerator from a shared enumerable. 此外,我们不想强制每个枚举器都支持并发使用,因为这样做会将不重要的开销添加到不需要它的多数情况,这意味着接口的使用者通常不能以任何方式依赖于这种情况。Further, we don't want to enforce that every enumerator support concurrent usage, as that would add non-trivial overheads to the majority case that doesn't require it, which means a consumer of the interface generally couldn't rely on this any way. - 主要:性能。Major: Performance. 此
MoveNextAsync
/Current
方法需要每个操作有两个接口调用,而最佳情况WaitForNextAsync
/TryGetNext
是大多数迭代都同步完成,同时启用一个紧密的内部循环,以便TryGetNext
每个操作仅有一个接口调用。TheMoveNextAsync
/Current
approach requires two interface calls per operation, whereas the best case forWaitForNextAsync
/TryGetNext
is that most iterations complete synchronously, enabling a tight inner loop withTryGetNext
, such that we only have one interface call per operation. 这在接口调用占据计算的情况下可能会产生实实在在的影响。This can have a measurable impact in situations where the interface calls dominate the computation.
不过,这种缺点不太重要,包括手动使用这些方法时显著增加的复杂性,以及使用它们时引入 bug 的可能性也会增加。However, there are non-trivial downsides, including significantly increased complexity when consuming these manually, and an increased chance of introducing bugs when using them. 尽管性能优势在 microbenchmarks 中显示,但我们并不相信它们会有影响力于绝大多数实际使用。And while the performance benefits show up in microbenchmarks, we don't believe they'll be impactful in the vast majority of real usage. 如果出现这种情况,我们可以用一种全新的方式引入一组接口。If it turns out they are, we can introduce a second set of interfaces in a light-up fashion.
丢弃的选项:Discarded options considered:
ValueTask<bool> WaitForNextAsync(); bool TryGetNext(out T result);
:out
参数不能是协变的。ValueTask<bool> WaitForNextAsync(); bool TryGetNext(out T result);
:out
parameters can't be covariant. 这里还会有一个小的影响, (一般的试用模式问题,) 这可能会导致引用类型结果出现运行时写入关卡。There's also a small impact here (an issue with the try pattern in general) that this likely incurs a runtime write barrier for reference type results.
取消Cancellation
可以通过多种方法来支持取消:There are several possible approaches to supporting cancellation:
IAsyncEnumerable<T>
/IAsyncEnumerator<T>
取消-不可知:CancellationToken
不显示在任何位置。IAsyncEnumerable<T>
/IAsyncEnumerator<T>
are cancellation-agnostic:CancellationToken
doesn't appear anywhere. 取消是通过以适当的方式将逻辑上收录CancellationToken
到可枚举的和/或枚举器来实现的,例如,在调用迭代器时,将CancellationToken
作为参数传递给迭代器方法并在迭代器的正文中使用它,就像使用任何其他参数完成的操作一样。Cancellation is achieved by logically baking theCancellationToken
into the enumerable and/or enumerator in whatever manner is appropriate, e.g. when calling an iterator, passing theCancellationToken
as an argument to the iterator method and using it in the body of the iterator, as is done with any other parameter.IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken)
:你将传递CancellationToken
到GetAsyncEnumerator
,后续MoveNextAsync
操作会对它进行处理。IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken)
: You pass aCancellationToken
toGetAsyncEnumerator
, and subsequentMoveNextAsync
operations respect it however it can.IAsyncEnumerator<T>.MoveNextAsync(CancellationToken)
:CancellationToken
向每个单独的MoveNextAsync
调用传递。IAsyncEnumerator<T>.MoveNextAsync(CancellationToken)
: You pass aCancellationToken
to each individualMoveNextAsync
call.- 1 && 2:这两个均嵌入
CancellationToken
到可枚举/枚举器,并传入CancellationToken
GetAsyncEnumerator
。1 && 2: You both embedCancellationToken
s into your enumerable/enumerator and passCancellationToken
s intoGetAsyncEnumerator
. - 1 && 3:这两个均嵌入
CancellationToken
到可枚举/枚举器,并传入CancellationToken
MoveNextAsync
。1 && 3: You both embedCancellationToken
s into your enumerable/enumerator and passCancellationToken
s intoMoveNextAsync
.
从纯粹的理论角度来看, (5) 是最可靠的,因为 (MoveNextAsync
接受的) CancellationToken
允许对取消的内容进行最精细的控制, (b) CancellationToken
只是可以作为参数传递到迭代器中的任何其他类型,等等。From a purely theoretical perspective, (5) is the most robust, in that (a) MoveNextAsync
accepting a CancellationToken
enables the most fine-grained control over what's canceled, and (b) CancellationToken
is just any other type that can passed as an argument into iterators, embedded in arbitrary types, etc.
但是,该方法有多个问题:However, there are multiple problems with that approach:
- 如何
CancellationToken
传递来GetAsyncEnumerator
使其成为迭代器的主体?How does aCancellationToken
passed toGetAsyncEnumerator
make it into the body of the iterator? 我们可能会公开一个新的iterator
关键字,你可以从该关键字开始,以访问CancellationToken
传递到的。GetEnumerator
但) 是众多额外机械,b) 我们要使其成为一流的公民,而 c) 99% 事例似乎是相同的代码调用迭代器并调用GetAsyncEnumerator
它,在这种情况下,它只是将CancellationToken
作为参数传递到方法中。We could expose a newiterator
keyword that you could dot off of to get access to theCancellationToken
passed toGetEnumerator
, but a) that's a lot of additional machinery, b) we're making it a very first-class citizen, and c) the 99% case would seem to be the same code both calling an iterator and callingGetAsyncEnumerator
on it, in which case it can just pass theCancellationToken
as an argument into the method. - 如何
CancellationToken
传递来进入MoveNextAsync
方法的主体?How does aCancellationToken
passed toMoveNextAsync
get into the body of the method? 更糟的是,就像在iterator
本地对象公开外,其值可以在等待期间更改,这意味着,使用令牌注册的任何代码都需要在等待之前从其取消注册,然后在每次调用后重新注册MoveNextAsync
,无论是由编译器在迭代器中还是由开发人员手动实现的,都需要在每次调用中进行注册和注销。This is even worse, as if it's exposed off of aniterator
local object, its value could change across awaits, which means any code that registered with the token would need to unregister from it prior to awaits and then re-register after; it's also potentially quite expensive to need to do such registering and unregistering in everyMoveNextAsync
call, regardless of whether implemented by the compiler in an iterator or by a developer manually. - 开发人员如何取消
foreach
循环?How does a developer cancel aforeach
loop? 如果它是通过CancellationToken
向可枚举/枚举器提供的,) 则需要支持 "对枚举器进行支持foreach
",从而将它们提升为第一类公民,现在你需要开始考虑枚举器周围构建的生态系统 (例如,LINQ 方法) 或 b) 我们需要将存储所CancellationToken
WithCancellation
提供的令牌的扩展方法之外的某些扩展方法嵌入到枚举中,然后将IAsyncEnumerable<T>
其传递给已包装的可枚举的。)GetAsyncEnumerator
GetAsyncEnumerator
(If it's done by giving aCancellationToken
to an enumerable/enumerator, then either a) we need to supportforeach
'ing over enumerators, which raises them to being first-class citizens, and now you need to start thinking about an ecosystem built up around enumerators (e.g. LINQ methods) or b) we need to embed theCancellationToken
in the enumerable anyway by having someWithCancellation
extension method off ofIAsyncEnumerable<T>
that would store the provided token and then pass it into the wrapped enumerable'sGetAsyncEnumerator
when theGetAsyncEnumerator
on the returned struct is invoked (ignoring that token). 或者,您可以仅使用CancellationToken
您在 foreach 的主体中所拥有的。Or, you can just use theCancellationToken
you have in the body of the foreach. - 如果支持查询理解,如何将
CancellationToken
提供给GetEnumerator
MoveNextAsync
每个子句或将其传递给每个子句呢?If/when query comprehensions are supported, how would theCancellationToken
supplied toGetEnumerator
orMoveNextAsync
be passed into each clause? 最简单的方法是让子句捕获它,此时将忽略传递到的任何标记GetAsyncEnumerator
/MoveNextAsync
。The easiest way would simply be for the clause to capture it, at which point whatever token is passed toGetAsyncEnumerator
/MoveNextAsync
is ignored.
本文档的早期版本建议 (1) ,但我们已切换到 (4) 。An earlier version of this document recommended (1), but we since switched to (4).
(1) 的两个主要问题:The two main problems with (1):
- 可取消枚举的创建者必须实现一些样本,并且只能利用编译器对异步迭代器的支持来实现
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken)
方法。producers of cancellable enumerables have to implement some boilerplate, and can only leverage the compiler's support for async-iterators to implement aIAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken)
method. - 很多的创建者可能只是将
CancellationToken
参数添加到其异步可枚举签名,而这会阻止使用者在给定类型时传递所需的取消令牌IAsyncEnumerable
。it is likely that many producers would be tempted to just add aCancellationToken
parameter to their async-enumerable signature instead, which will prevent consumers from passing the cancellation token they want when they are given anIAsyncEnumerable
type.
有两种主要的消耗方案:There are two main consumption scenarios:
await foreach (var i in GetData(token)) ...
其中,使用者调用 async 迭代器方法。await foreach (var i in GetData(token)) ...
where the consumer calls the async-iterator method,await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ...
使用者处理给定实例的位置IAsyncEnumerable
。await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ...
where the consumer deals with a givenIAsyncEnumerable
instance.
我们发现一种合理的折衷方法是以一种可方便地使用异步流的生成者和使用者的方式来支持这两种方案。We find that a reasonable compromise to support both scenarios in a way that is convenient for both producers and consumers of async-streams is to use a specially annotated parameter in the async-iterator method. [EnumeratorCancellation]
特性用于实现此目的。The [EnumeratorCancellation]
attribute is used for this purpose. 如果将此属性放置在参数上,则会告诉编译器如果将令牌传递给 GetAsyncEnumerator
方法,应使用该令牌,而不是最初为参数传递的值。Placing this attribute on a parameter tells the compiler that if a token is passed to the GetAsyncEnumerator
method, that token should be used instead of the value originally passed for the parameter.
以 IAsyncEnumerable<int> GetData([EnumeratorCancellation] CancellationToken token = default)
为例。Consider IAsyncEnumerable<int> GetData([EnumeratorCancellation] CancellationToken token = default)
. 此方法的实施者只需使用方法体中的参数即可。The implementer of this method can simply use the parameter in the method body. 使用者可以使用上面的两种消费模式:The consumer can use either consumption patterns above:
- 如果使用
GetData(token)
,则该令牌将保存到 async 可枚举中,并在迭代中使用。if you useGetData(token)
, then the token is saved into the async-enumerable and will be used in iteration, - 如果使用
givenIAsyncEnumerable.WithCancellation(token)
,则传递给的令牌GetAsyncEnumerator
将取代所有保存在异步可枚举中的令牌。if you usegivenIAsyncEnumerable.WithCancellation(token)
, then the token passed toGetAsyncEnumerator
will supersede any token saved in the async-enumerable.
foreachforeach
foreach
除了支持的现有支持外,还将扩充到支持 IAsyncEnumerable<T>
IEnumerable<T>
。foreach
will be augmented to support IAsyncEnumerable<T>
in addition to its existing support for IEnumerable<T>
. 它支持作为一种模式的等效项 IAsyncEnumerable<T>
,如果相关成员公开公开,而不是直接使用接口,则要启用基于结构的扩展,以避免分配,并使用替代等待作为和的返回类型 MoveNextAsync
DisposeAsync
。And it will support the equivalent of IAsyncEnumerable<T>
as a pattern if the relevant members are exposed publicly, falling back to using the interface directly if not, in order to enable struct-based extensions that avoid allocating as well as using alternative awaitables as the return type of MoveNextAsync
and DisposeAsync
.
语法Syntax
使用以下语法:Using the syntax:
foreach (var i in enumerable)
C # 将继续视为 enumerable
同步可枚举,这样,即使它公开了用于 async 枚举的相关 api (公开模式或实现接口) ,它也只会考虑同步 api。C# will continue to treat enumerable
as a synchronous enumerable, such that even if it exposes the relevant APIs for async enumerables (exposing the pattern or implementing the interface), it will only consider the synchronous APIs.
若要强制 foreach
改为仅考虑异步 api,请按 await
如下所示插入:To force foreach
to instead only consider the asynchronous APIs, await
is inserted as follows:
await foreach (var i in enumerable)
不会提供支持使用 async 或 sync Api 的语法;开发人员必须根据所使用的语法进行选择。No syntax would be provided that would support using either the async or the sync APIs; the developer must choose based on the syntax used.
丢弃的选项:Discarded options considered:
foreach (var i in await enumerable)
:这是有效的语法,并且更改其含义会是一项重大更改。foreach (var i in await enumerable)
: This is already valid syntax, and changing its meaning would be a breaking change. 这意味着await
,将enumerable
从可迭代中返回同步的内容,然后同步循环访问该内容。This means toawait
theenumerable
, get back something synchronously iterable from it, and then synchronously iterate through that.foreach (var i await in enumerable)
、foreach (var await i in enumerable)
、foreach (await var i in enumerable)
:这一切都表示我们正在等待下一项,但在 foreach 中涉及其他等待,特别是如果可枚举的是IAsyncDisposable
,我们将是await
其异步处理。foreach (var i await in enumerable)
,foreach (var await i in enumerable)
,foreach (await var i in enumerable)
: These all suggest that we're awaiting the next item, but there are other awaits involved in foreach, in particular if the enumerable is anIAsyncDisposable
, we will beawait
'ing its async disposal. 该 await 作为 foreach 的作用域,而不是每个单个元素的作用域,因此await
关键字一定要处于foreach
级别。That await is as the scope of the foreach rather than for each individual element, and thus theawait
keyword deserves to be at theforeach
level. 此外,将其与关联后,便可以使用foreach
foreach
不同的术语(例如 "await foreach")来描述。Further, having it associated with theforeach
gives us a way to describe theforeach
with a different term, e.g. a "await foreach". 但更重要的是,在foreach
将语法作为语法的同时考虑语法using
,使其保持一致,并且using (await ...)
已经是有效的语法。But more importantly, there's value in consideringforeach
syntax at the same time asusing
syntax, so that they remain consistent with each other, andusing (await ...)
is already valid syntax.foreach await (var i in enumerable)
仍要考虑:Still to consider:
foreach
今天不支持循环访问枚举器。foreach
today does not support iterating through an enumerator. 我们期望更常见的方法是,这种方法更常见IAsyncEnumerator<T>
,因此这await foreach
同时支持IAsyncEnumerable<T>
和IAsyncEnumerator<T>
。We expect it will be more common to haveIAsyncEnumerator<T>
s handed around, and thus it's tempting to supportawait foreach
with bothIAsyncEnumerable<T>
andIAsyncEnumerator<T>
. 但一旦我们添加了此类支持,它就会引入是否IAsyncEnumerator<T>
为第一类公民的问题,以及是否需要对枚举器进行组合器和枚举的重载?But once we add such support, it introduces the question of whetherIAsyncEnumerator<T>
is a first-class citizen, and whether we need to have overloads of combinators that operate on enumerators in addition to enumerables? 我们想要鼓励方法返回枚举器而不是枚举?Do we want to encourage methods to return enumerators rather than enumerables? 我们应该继续讨论这一点。We should continue to discuss this. 如果我们决定不想对其提供支持,我们可能希望引入一个扩展方法public static IAsyncEnumerable<T> AsEnumerable<T>(this IAsyncEnumerator<T> enumerator);
,该方法允许枚举器仍为foreach
。If we decide we don't want to support it, we might want to introduce an extension methodpublic static IAsyncEnumerable<T> AsEnumerable<T>(this IAsyncEnumerator<T> enumerator);
that would allow an enumerator to still beforeach
'd. 如果我们决定要对其提供支持,我们还需要决定是否要对await foreach
DisposeAsync
枚举器调用,答案是否为 "不,控制过度处置应由调用者处理GetEnumerator
"。If we decide we do want to support it, we'll need to also decide on whether theawait foreach
would be responsible for callingDisposeAsync
on the enumerator, and the answer is likely "no, control over disposal should be handled by whoever calledGetEnumerator
."
基于模式的编译Pattern-based Compilation
编译器将绑定到基于模式的 Api (如果存在),如果使用接口 (模式,则最好使用) 的实例方法或扩展方法满足此模式。The compiler will bind to the pattern-based APIs if they exist, preferring those over using the interface (the pattern may be satisfied with instance methods or extension methods). 模式的要求如下:The requirements for the pattern are:
- 可枚举必须公开一个
GetAsyncEnumerator
方法,该方法可在没有参数的情况调用,并返回一个满足相关模式的枚举器。The enumerable must expose aGetAsyncEnumerator
method that may be called with no arguments and that returns an enumerator that meets the relevant pattern. - 枚举器必须公开一个
MoveNextAsync
方法,该方法可在没有参数的情况调用,并返回可能是await
ed 并GetResult()
返回的内容bool
。The enumerator must expose aMoveNextAsync
method that may be called with no arguments and that returns something which may beawait
ed and whoseGetResult()
returns abool
. - 枚举器还必须公开
Current
属性,该属性的 getter 返回T
表示所枚举的数据类型的。The enumerator must also exposeCurrent
property whose getter returns aT
representing the kind of data being enumerated. - 枚举器可以选择公开一个
DisposeAsync
方法,该方法可在不使用任何参数的情况调用,并返回可以是await
ed 并返回的内容GetResult()
void
。The enumerator may optionally expose aDisposeAsync
method that may be invoked with no arguments and that returns something that can beawait
ed and whoseGetResult()
returnsvoid
.
此代码:This code:
var enumerable = ...;
await foreach (T item in enumerable)
{
...
}
转换为的等效项:is translated to the equivalent of:
var enumerable = ...;
var enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
T item = enumerator.Current;
...
}
}
finally
{
await enumerator.DisposeAsync(); // omitted, along with the try/finally, if the enumerator doesn't expose DisposeAsync
}
如果迭代类型未公开正确的模式,则将使用这些接口。If the iterated type doesn't expose the right pattern, the interfaces will be used.
ConfigureAwaitConfigureAwait
此基于模式的编译允许 ConfigureAwait
通过扩展方法在所有等待中使用 ConfigureAwait
:This pattern-based compilation will allow ConfigureAwait
to be used on all of the awaits, via a ConfigureAwait
extension method:
await foreach (T item in enumerable.ConfigureAwait(false))
{
...
}
这将基于我们还将添加到 .NET 的类型,可能会 System.Threading.Tasks.Extensions.dll:This will be based on types we'll add to .NET as well, likely to 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 Enumerator
{
private readonly IAsyncEnumerator<T> _enumerator;
private readonly bool _continueOnCapturedContext;
internal Enumerator(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);
}
}
}
}
请注意,此方法不会允许 ConfigureAwait
与基于模式的枚举一起使用。但在这种情况下, ConfigureAwait
只是在上显示为扩展 Task
/ Task<T>
/ ValueTask
/ ValueTask<T>
,而不能应用于任意可等待的东西,因为它仅在应用于任务时才有意义, (它控制在任务的继续支持) 中实现的行为,因此,在使用可等待可能不是任务的模式时,这并不合理。Note that this approach will not enable ConfigureAwait
to be used with pattern-based enumerables, but then again it's already the case that the ConfigureAwait
is only exposed as an extension on Task
/Task<T>
/ValueTask
/ValueTask<T>
and can't be applied to arbitrary awaitable things, as it only makes sense when applied to Tasks (it controls a behavior implemented in Task's continuation support), and thus doesn't make sense when using a pattern where the awaitable things may not be tasks. 返回可等待的任何人都可以在此类高级方案中提供自己的自定义行为。Anyone returning awaitable things can provide their own custom behavior in such advanced scenarios.
(如果我们可以通过某种方式来支持作用域或程序集级别的 ConfigureAwait
解决方案,则不需要这样做。 ) (If we can come up with some way to support a scope- or assembly-level ConfigureAwait
solution, then this won't be necessary.)
异步迭代器Async Iterators
语言/编译器 IAsyncEnumerable<T>
IAsyncEnumerator<T>
除了使用它们外,还支持生成和。The language / compiler will support producing IAsyncEnumerable<T>
s and IAsyncEnumerator<T>
s in addition to consuming them. 如今,该语言支持编写迭代器,如下所示:Today the language supports writing an iterator like:
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");
}
}
但 await
不能在这些迭代器的主体中使用。but await
can't be used in the body of these iterators. 我们将添加该支持。We will add that support.
语法Syntax
迭代器的现有语言支持根据方法是否包含任何来推断方法的迭代器特性 yield
。The existing language support for iterators infers the iterator nature of the method based on whether it contains any yield
s. 对于异步迭代器,情况也是如此。The same will be true for async iterators. 此类异步迭代器将 demarcated,并通过将添加到签名来区别于同步迭代器 async
,还必须将 IAsyncEnumerable<T>
或 IAsyncEnumerator<T>
作为其返回类型。Such async iterators will be demarcated and differentiated from synchronous iterators via adding async
to the signature, and must then also have either IAsyncEnumerable<T>
or IAsyncEnumerator<T>
as its return type. 例如,可以将上面的示例编写为异步迭代器,如下所示:For example, the above example could be written as an async iterator as follows:
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");
}
}
考虑的替代方案:Alternatives considered:
async
在签名中不使用:async
编译器可能需要使用,因为它使用它来确定await
在该上下文中是否有效。Not usingasync
in the signature: Usingasync
is likely technically required by the compiler, as it uses it to determine whetherawait
is valid in that context. 但是,即使不是必需的,我们也建立了await
只能在标记为的方法中使用,因此async
保持一致性非常重要。But even if it's not required, we've established thatawait
may only be used in methods marked asasync
, and it seems important to keep the consistency.- 为
IAsyncEnumerable<T>
启用自定义生成器:这是我们在将来可以查看的一些东西,但这是一项很复杂的工作,我们不支持同步的对应项。Enabling custom builders forIAsyncEnumerable<T>
: That's something we could look at for the future, but the machinery is complicated and we don't support that for the synchronous counterparts. iterator
签名中包含关键字:异步迭代器将async iterator
在签名中使用,并且yield
只能在async
包含的方法中使用; 然后,在iterator
同步迭代器上将其设置为iterator
可选。Having aniterator
keyword in the signature: Async iterators would useasync iterator
in the signature, andyield
could only be used inasync
methods that includediterator
;iterator
would then be made optional on synchronous iterators. 根据您的观点,这一优点在于,无论是否允许,该方法的签名都可以非常清楚地表明yield
方法是否确实要返回类型为的实例,IAsyncEnumerable<T>
而不是根据代码是否使用而制造yield
。Depending on your perspective, this has the benefit of making it very clear by the signature of the method whetheryield
is allowed and whether the method is actually meant to return instances of typeIAsyncEnumerable<T>
rather than the compiler manufacturing one based on whether the code usesyield
or not. 但这不同于同步迭代器,而不能对其进行要求。But it is different from synchronous iterators, which don't and can't be made to require one. 此外,某些开发人员不喜欢额外的语法。Plus some developers don't like the extra syntax. 如果我们是从头开始设计的,则可能需要这样做,但此时,将异步迭代器保持在同步迭代器附近会有更多的价值。If we were designing it from scratch, we'd probably make this required, but at this point there's much more value in keeping async iterators close to sync iterators.
LINQLINQ
类上有超过200的方法重载,所有这些重载都 System.Linq.Enumerable
适用于 IEnumerable<T>
; 其中一些接受 IEnumerable<T>
,其中一些接受,其中的一些是 IEnumerable<T>
。There are over ~200 overloads of methods on the System.Linq.Enumerable
class, all of which work in terms of IEnumerable<T>
; some of these accept IEnumerable<T>
, some of them produce IEnumerable<T>
, and many do both. 添加 LINQ 支持 IAsyncEnumerable<T>
可能需要为其复制所有这些重载,为另一个 ~ 200。Adding LINQ support for IAsyncEnumerable<T>
would likely entail duplicating all of these overloads for it, for another ~200. 由于在 IAsyncEnumerator<T>
异步环境中与在同步环境中的独立实体相比,可能更常见 IEnumerator<T>
,因此,我们可能需要另一个 ~ 200 重载来处理 IAsyncEnumerator<T>
。And since IAsyncEnumerator<T>
is likely to be more common as a standalone entity in the asynchronous world than IEnumerator<T>
is in the synchronous world, we could potentially need another ~200 overloads that work with IAsyncEnumerator<T>
. 此外,大量重载处理 (的谓词,例如,使用 Where
Func<T, bool>
) ,因此,可能需要基于的 IAsyncEnumerable<T>
重载处理同步和异步谓词 (例如, Func<T, ValueTask<bool>>
除了 Func<T, bool>
) 。Plus, a large number of the overloads deal with predicates (e.g. Where
that takes a Func<T, bool>
), and it may be desirable to have IAsyncEnumerable<T>
-based overloads that deal with both synchronous and asynchronous predicates (e.g. Func<T, ValueTask<bool>>
in addition to Func<T, bool>
). 虽然这并不适用于所有现在 ~ 400 的新重载,但大致的计算是它适用于一半,这意味着另一个 ~ 200 重载,总共 ~ 600 个新方法。While this isn't applicable to all of the now ~400 new overloads, a rough calculation is that it'd be applicable to half, which means another ~200 overloads, for a total of ~600 new methods.
这是一种惊人的 Api,在考虑到 (Ix) 这样的扩展库时,可能会有更多的 Api。That is a staggering number of APIs, with the potential for even more when extension libraries like Interactive Extensions (Ix) are considered. 但 Ix 已经实现了其中的许多,但看起来不是复制该工作的重要原因;当开发人员希望将 LINQ 与结合使用时,我们应帮助社区改进 Ix 并建议使用 IAsyncEnumerable<T>
。But Ix already has an implementation of many of these, and there doesn't seem to be a great reason to duplicate that work; we should instead help the community improve Ix and recommend it for when developers want to use LINQ with IAsyncEnumerable<T>
.
此外,还存在与查询理解语法有关的问题。There is also the issue of query comprehension syntax. 查询理解的基于模式的性质使其能够 "只使用" 一些运算符,例如,如果 Ix 提供以下方法:The pattern-based nature of query comprehensions would allow them to "just work" with some operators, e.g. if Ix provides the following methods:
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);
此 c # 代码将 "只工作":then this C# code will "just work":
IAsyncEnumerable<int> enumerable = ...;
IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select item * 2;
但是,没有支持在子句中使用的查询理解语法 await
,因此,如果添加了 Ix,例如:However, there is no query comprehension syntax that supports using await
in the clauses, so if Ix added, for example:
public static IAsyncEnumerable<TResult> Select<TSource, TResult>(this IAsyncEnumerable<TSource> source, Func<TSource, ValueTask<TResult>> func);
这会 "只工作":then this would "just work":
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;
}
但无法在子句中将它与内联一起写入 await
select
。but there'd be no way to write it with the await
inline in the select
clause. 作为一个单独的工作,我们可能会探讨 async { ... }
如何向语言添加表达式,此时,我们可以允许它们在 query 理解中使用,而上述操作可以编写为:As a separate effort, we could look into adding async { ... }
expressions to the language, at which point we could allow them to be used in query comprehensions and the above could instead be written as:
IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select async
{
await Task.Yield();
return item * 2;
};
若要启用 await
直接在表达式中使用,例如通过支持 async from
。or to enabling await
to be used directly in expressions, such as by supporting async from
. 但是,这种设计不太可能会对功能集的其余部分产生任何影响,并且这不是我们目前要投入的高价值的东西,所以建议不要在此处执行任何其他操作。However, it's unlikely a design here would impact the rest of the feature set one way or the other, and this isn't a particularly high-value thing to invest in right now, so the proposal is to do nothing additional here right now.
与其他异步框架集成Integration with other asynchronous frameworks
与 IObservable<T>
和其他异步框架的集成 (例如,将在库级别而不是在语言级别执行反应流) 。Integration with IObservable<T>
and other asynchronous frameworks (e.g. reactive streams) would be done at the library level rather than at the language level. 例如,可以将中的所有数据 IAsyncEnumerator<T>
发布到 IObserver<T>
,只需通过 "对 await foreach
枚举器执行" 操作,并将 OnNext
数据放到观察器,这样 AsObservable<T>
就可以实现扩展方法。For example, all of the data from an IAsyncEnumerator<T>
can be published to an IObserver<T>
simply by await foreach
'ing over the enumerator and OnNext
'ing the data to the observer, so an AsObservable<T>
extension method is possible. IObservable<T>
如果在中使用 await foreach
,则需要缓冲数据 (以防在前一项仍在处理) 的情况下推送另一项,但可以轻松实现此类推送请求适配器,以便能够 IObservable<T>
从中请求 IAsyncEnumerator<T>
。Consuming an IObservable<T>
in a await foreach
requires buffering the data (in case another item is pushed while the previous item is still being processing), but such a push-pull adapter can easily be implemented to enable an IObservable<T>
to be pulled from with an IAsyncEnumerator<T>
. 等. Rx/Ix 已经提供此类实现的原型,并提供 https://github.com/dotnet/corefx/tree/master/src/System.Threading.Channels 各种类型的缓冲数据结构。Etc. Rx/Ix already provide prototypes of such implementations, and libraries like https://github.com/dotnet/corefx/tree/master/src/System.Threading.Channels provide various kinds of buffering data structures. 此阶段不需要涉及此语言。The language need not be involved at this stage.