public static IAsyncEnumerable <IList <TSource> > Buffer <TSource>(this IAsyncEnumerable <TSource> source, int count, int skip) { if (source == null) { throw Error.ArgumentNull(nameof(source)); } if (count <= 0) { throw Error.ArgumentOutOfRange(nameof(count)); } if (skip <= 0) { throw Error.ArgumentOutOfRange(nameof(skip)); } #if USE_ASYNC_ITERATOR return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <IList <TSource> > Core(CancellationToken cancellationToken) { var buffers = new Queue <IList <TSource> >(); var index = 0; await foreach (var item in AsyncEnumerableExtensions.WithCancellation(source, cancellationToken).ConfigureAwait(false)) { if (index++ % skip == 0) { buffers.Enqueue(new List <TSource>(count)); } foreach (var buffer in buffers) { buffer.Add(item); } if (buffers.Count > 0 && buffers.Peek().Count == count) { yield return(buffers.Dequeue()); } } while (buffers.Count > 0) { yield return(buffers.Dequeue()); } } #else return(new BufferAsyncIterator <TSource>(source, count, skip)); #endif }
private static IAsyncEnumerable <TSource> CatchCore <TSource>(IEnumerable <IAsyncEnumerable <TSource> > sources) { #if USE_ASYNC_ITERATOR return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { var error = default(ExceptionDispatchInfo); foreach (var source in sources) { await using (var e = source.GetConfiguredAsyncEnumerator(cancellationToken, false)) { error = null; while (true) { var c = default(TSource); try { if (!await e.MoveNextAsync()) { break; } c = e.Current; } catch (Exception ex) { error = ExceptionDispatchInfo.Capture(ex); break; } yield return(c); } if (error == null) { break; } } } error?.Throw(); } #else return(new CatchAsyncIterator <TSource>(sources)); #endif }
private static IAsyncEnumerable <TSource> DoCore <TSource>(IAsyncEnumerable <TSource> source, Func <TSource, CancellationToken, Task> onNext, Func <Exception, CancellationToken, Task> onError, Func <CancellationToken, Task> onCompleted) { #if USE_ASYNC_ITERATOR return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { await using (var e = source.GetConfiguredAsyncEnumerator(cancellationToken, false)) { while (true) { TSource item; try { if (!await e.MoveNextAsync()) { break; } item = e.Current; await onNext(item, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch (Exception ex) when(onError != null) { await onError(ex, cancellationToken).ConfigureAwait(false); throw; } yield return(item); } if (onCompleted != null) { await onCompleted(cancellationToken).ConfigureAwait(false); } } } #else return(new DoAsyncIteratorWithTaskAndCancellation <TSource>(source, onNext, onError, onCompleted)); #endif }
/// <summary> /// Repeats the element indefinitely. /// </summary> /// <typeparam name="TResult">The type of the elements in the source sequence.</typeparam> /// <param name="element">Element to repeat.</param> /// <returns>The async-enumerable sequence producing the element repeatedly and sequentially.</returns> public static IAsyncEnumerable <TResult> Repeat <TResult>(TResult element) { return(AsyncEnumerable.Create(Core)); #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously async IAsyncEnumerator <TResult> Core(CancellationToken cancellationToken) { while (true) { cancellationToken.ThrowIfCancellationRequested(); yield return(element); } } #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously }
/// <summary> /// Returns an async-enumerable sequence that starts the specified cancellable asynchronous factory function whenever a new observer subscribes. /// The CancellationToken passed to the asynchronous factory function is tied to the returned disposable subscription, allowing best-effort cancellation. /// </summary> /// <typeparam name="TSource">The type of the elements in the sequence returned by the factory function, and in the resulting sequence.</typeparam> /// <param name="factory">Asynchronous factory function, supporting cancellation, to start for each consumer that starts enumerating the resulting asynchronous sequence.</param> /// <returns>An async-enumerable sequence whose observers trigger the given asynchronous async-enumerable factory function to be started.</returns> /// <exception cref="ArgumentNullException"><paramref name="factory"/> is null.</exception> /// <remarks>This operator is especially useful in conjunction with the asynchronous programming features introduced in C# 5.0 and Visual Basic 11.</remarks> /// <remarks>When a subscription to the resulting sequence is disposed, the CancellationToken that was fed to the asynchronous async-enumerable factory function will be signaled.</remarks> public static IAsyncEnumerable <TSource> Defer <TSource>(Func <CancellationToken, Task <IAsyncEnumerable <TSource> > > factory) { if (factory == null) { throw Error.ArgumentNull(nameof(factory)); } return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { await foreach (var item in (await factory(cancellationToken).ConfigureAwait(false)).WithCancellation(cancellationToken).ConfigureAwait(false)) { yield return(item); } } }
/// <summary> /// Ignores all elements in an async-enumerable sequence leaving only the termination messages. /// </summary> /// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam> /// <param name="source">Source sequence.</param> /// <returns>An empty async-enumerable sequence that signals termination, successful or exceptional, of the source sequence.</returns> /// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception> public static IAsyncEnumerable <TSource> IgnoreElements <TSource>(this IAsyncEnumerable <TSource> source) { if (source == null) { throw Error.ArgumentNull(nameof(source)); } return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { await foreach (var _ in source.WithCancellation(cancellationToken).ConfigureAwait(false)) { } yield break; } }
private static IAsyncEnumerable <TSource> DistinctUntilChangedCore <TSource, TKey>(IAsyncEnumerable <TSource> source, Func <TSource, CancellationToken, ValueTask <TKey> > keySelector, IEqualityComparer <TKey> comparer) { #if USE_ASYNC_ITERATOR if (comparer == null) { comparer = EqualityComparer <TKey> .Default; } return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { await using (var e = source.GetConfiguredAsyncEnumerator(cancellationToken, false)) { if (!await e.MoveNextAsync()) { yield break; } var item = e.Current; var latestKey = await keySelector(item, cancellationToken).ConfigureAwait(false); yield return(item); while (await e.MoveNextAsync()) { item = e.Current; var currentKey = await keySelector(item, cancellationToken).ConfigureAwait(false); if (!comparer.Equals(latestKey, currentKey)) { latestKey = currentKey; yield return(item); } } } } #else return(new DistinctUntilChangedAsyncIteratorWithTaskAndCancellation <TSource, TKey>(source, keySelector, comparer)); #endif }
private static IAsyncEnumerable <TSource> DoCore <TSource>(IAsyncEnumerable <TSource> source, Func <TSource, Task> onNext, Func <Exception, Task>?onError, Func <Task>?onCompleted) { return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { await using var e = source.GetConfiguredAsyncEnumerator(cancellationToken, false); while (true) { TSource item; try { if (!await e.MoveNextAsync()) { break; } item = e.Current; await onNext(item).ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch (Exception ex) when(onError != null) { await onError(ex).ConfigureAwait(false); throw; } yield return(item); } if (onCompleted != null) { await onCompleted().ConfigureAwait(false); } } }
/// <summary> /// Repeats the async-enumerable sequence indefinitely. /// </summary> /// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam> /// <param name="source">Observable sequence to repeat.</param> /// <returns>The async-enumerable sequence producing the elements of the given sequence repeatedly and sequentially.</returns> /// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception> public static IAsyncEnumerable <TSource> Repeat <TSource>(this IAsyncEnumerable <TSource> source) { if (source == null) { throw Error.ArgumentNull(nameof(source)); } return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { while (true) { await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) { yield return(item); } } } }
public static IAsyncEnumerable <TSource> Concat <TSource>(params IAsyncEnumerable <TSource>[] sources) { if (sources == null) { throw Error.ArgumentNull(nameof(sources)); } return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { foreach (var source in sources) { await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) { yield return(item); } } } }
public static IAsyncEnumerable <TSource> Defer <TSource>(Func <Task <IAsyncEnumerable <TSource> > > factory) { if (factory == null) { throw Error.ArgumentNull(nameof(factory)); } #if USE_ASYNC_ITERATOR return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { await foreach (var item in (await factory().ConfigureAwait(false)).WithCancellation(cancellationToken).ConfigureAwait(false)) { yield return(item); } } #else return(new AsyncDeferIterator <TSource>(factory)); #endif }
private static IAsyncEnumerable <TSource> DistinctUntilChangedCore <TSource>(IAsyncEnumerable <TSource> source, IEqualityComparer <TSource> comparer) { #if USE_ASYNC_ITERATOR if (comparer == null) { comparer = EqualityComparer <TSource> .Default; } return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { await using (var e = source.GetConfiguredAsyncEnumerator(cancellationToken, false)) { if (!await e.MoveNextAsync()) { yield break; } var latest = e.Current; yield return(latest); while (await e.MoveNextAsync()) { var item = e.Current; if (!comparer.Equals(latest, item)) { latest = item; yield return(latest); } } } } #else return(new DistinctUntilChangedAsyncIterator <TSource>(source, comparer)); #endif }
public static IAsyncEnumerable <TSource> IgnoreElements <TSource>(this IAsyncEnumerable <TSource> source) { if (source == null) { throw Error.ArgumentNull(nameof(source)); } #if USE_ASYNC_ITERATOR return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { await foreach (var _ in AsyncEnumerableExtensions.WithCancellation(source, cancellationToken).ConfigureAwait(false)) { } yield break; } #else return(new IgnoreElementsAsyncIterator <TSource>(source)); #endif }
public static IAsyncEnumerable <IList <TSource> > Buffer <TSource>(this IAsyncEnumerable <TSource> source, int count) { if (source == null) { throw Error.ArgumentNull(nameof(source)); } if (count <= 0) { throw Error.ArgumentOutOfRange(nameof(count)); } #if USE_ASYNC_ITERATOR return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <IList <TSource> > Core(CancellationToken cancellationToken) { var buffer = new List <TSource>(count); await foreach (var item in AsyncEnumerableExtensions.WithCancellation(source, cancellationToken).ConfigureAwait(false)) { buffer.Add(item); if (buffer.Count == count) { yield return(buffer); buffer = new List <TSource>(count); } } if (buffer.Count > 0) { yield return(buffer); } } #else return(new BufferAsyncIterator <TSource>(source, count, count)); #endif }
private static IAsyncEnumerable <TSource> DistinctUntilChangedCore <TSource, TKey>(IAsyncEnumerable <TSource> source, Func <TSource, CancellationToken, ValueTask <TKey> > keySelector, IEqualityComparer <TKey>?comparer) { comparer ??= EqualityComparer <TKey> .Default; return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { await using var e = source.GetConfiguredAsyncEnumerator(cancellationToken, false); if (!await e.MoveNextAsync()) { yield break; } var item = e.Current; var latestKey = await keySelector(item, cancellationToken).ConfigureAwait(false); yield return(item); while (await e.MoveNextAsync()) { item = e.Current; var currentKey = await keySelector(item, cancellationToken).ConfigureAwait(false); // REVIEW: Need comparer!.Equals to satisfy nullable reference type warnings. if (!comparer !.Equals(latestKey, currentKey)) { latestKey = currentKey; yield return(item); } } } }
/// <summary> /// Constructs an async-enumerable sequence that depends on a resource object, whose lifetime is tied to the resulting async-enumerable sequence's lifetime. The resource is obtained and used through asynchronous methods. /// The CancellationToken passed to the asynchronous methods is tied to the returned disposable subscription, allowing best-effort cancellation at any stage of the resource acquisition or usage. /// </summary> /// <typeparam name="TSource">The type of the elements in the produced sequence.</typeparam> /// <typeparam name="TResource">The type of the resource used during the generation of the resulting sequence. Needs to implement <see cref="IDisposable"/>.</typeparam> /// <param name="resourceFactory">Asynchronous factory function to obtain a resource object.</param> /// <param name="enumerableFactory">Asynchronous factory function to obtain an async-enumerable sequence that depends on the obtained resource.</param> /// <returns>An async-enumerable sequence whose lifetime controls the lifetime of the dependent resource object.</returns> /// <exception cref="ArgumentNullException"><paramref name="resourceFactory"/> or <paramref name="enumerableFactory"/> is null.</exception> /// <remarks>This operator is especially useful in conjunction with the asynchronous programming features introduced in C# 5.0 and Visual Basic 11.</remarks> /// <remarks>When a subscription to the resulting sequence is disposed, the CancellationToken that was fed to the asynchronous resource factory and async-enumerable factory functions will be signaled.</remarks> public static IAsyncEnumerable <TSource> Using <TSource, TResource>(Func <CancellationToken, Task <TResource> > resourceFactory, Func <TResource, CancellationToken, ValueTask <IAsyncEnumerable <TSource> > > enumerableFactory) where TResource : IDisposable { if (resourceFactory == null) { throw Error.ArgumentNull(nameof(resourceFactory)); } if (enumerableFactory == null) { throw Error.ArgumentNull(nameof(enumerableFactory)); } return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { using var resource = await resourceFactory(cancellationToken).ConfigureAwait(false); await foreach (var item in (await enumerableFactory(resource, cancellationToken).ConfigureAwait(false)).WithCancellation(cancellationToken).ConfigureAwait(false)) { yield return(item); } } }
public static IAsyncEnumerable <TSource> Scan <TSource>(this IAsyncEnumerable <TSource> source, Func <TSource, TSource, ValueTask <TSource> > accumulator) { if (source == null) { throw Error.ArgumentNull(nameof(source)); } if (accumulator == null) { throw Error.ArgumentNull(nameof(accumulator)); } #if USE_ASYNC_ITERATOR return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { await using (var e = source.GetConfiguredAsyncEnumerator(cancellationToken, false)) { if (!await e.MoveNextAsync()) { yield break; } var res = e.Current; while (await e.MoveNextAsync()) { res = await accumulator(res, e.Current).ConfigureAwait(false); yield return(res); } } } #else return(new ScanAsyncEnumerableWithTask <TSource>(source, accumulator)); #endif }
public static IAsyncEnumerable <TSource> Concat <TSource>(this IEnumerable <IAsyncEnumerable <TSource> > sources) { if (sources == null) { throw Error.ArgumentNull(nameof(sources)); } #if USE_ASYNC_ITERATOR return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { foreach (var source in sources) { await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) { yield return(item); } } } #else return(ConcatCore(sources)); #endif }
/// <summary> /// Repeats the async-enumerable sequence a specified number of times. /// </summary> /// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam> /// <param name="source">Observable sequence to repeat.</param> /// <param name="count">Number of times to repeat the sequence.</param> /// <returns>The async-enumerable sequence producing the elements of the given sequence repeatedly.</returns> /// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception> /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is less than zero.</exception> public static IAsyncEnumerable <TSource> Repeat <TSource>(this IAsyncEnumerable <TSource> source, int count) { if (source == null) { throw Error.ArgumentNull(nameof(source)); } if (count < 0) { throw Error.ArgumentOutOfRange(nameof(count)); } return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { for (var i = 0; i < count; i++) { await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) { yield return(item); } } } }
public static IAsyncEnumerable <TSource> Repeat <TSource>(this IAsyncEnumerable <TSource> source) { if (source == null) { throw Error.ArgumentNull(nameof(source)); } #if USE_ASYNC_ITERATOR return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { while (true) { await foreach (var item in AsyncEnumerableExtensions.WithCancellation(source, cancellationToken).ConfigureAwait(false)) { yield return(item); } } } #else return(new RepeatSequenceAsyncIterator <TSource>(source, -1)); #endif }
/// <summary> /// Invokes a specified action after the source async-enumerable sequence terminates gracefully or exceptionally. /// </summary> /// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam> /// <param name="source">Source sequence.</param> /// <param name="finallyAction">Action to invoke after the source async-enumerable sequence terminates.</param> /// <returns>Source sequence with the action-invoking termination behavior applied.</returns> /// <exception cref="ArgumentNullException"><paramref name="source"/> or <paramref name="finallyAction"/> is null.</exception> public static IAsyncEnumerable<TSource> Finally<TSource>(this IAsyncEnumerable<TSource> source, Action finallyAction) { if (source == null) throw Error.ArgumentNull(nameof(source)); if (finallyAction == null) throw Error.ArgumentNull(nameof(finallyAction)); return AsyncEnumerable.Create(Core); async IAsyncEnumerator<TSource> Core(CancellationToken cancellationToken) { try { await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) { yield return item; } } finally { finallyAction(); } } }
public static IAsyncEnumerable <TSource> Amb <TSource>(params IAsyncEnumerable <TSource>[] sources) { if (sources == null) { throw Error.ArgumentNull(nameof(sources)); } return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { // // REVIEW: See remarks on binary overload for changes compared to the original. // var n = sources.Length; var enumerators = new IAsyncEnumerator <TSource> [n]; var moveNexts = new Task <bool> [n]; var individualTokenSources = new CancellationTokenSource[n]; for (var i = 0; i < n; i++) { individualTokenSources[i] = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); } try { for (var i = 0; i < n; i++) { var enumerator = sources[i].GetAsyncEnumerator(individualTokenSources[i].Token); enumerators[i] = enumerator; moveNexts[i] = enumerator.MoveNextAsync().AsTask(); } } catch { var cleanup = new Task[n]; for (var i = n - 1; i >= 0; i--) { individualTokenSources[i].Cancel(); cleanup[i] = AwaitMoveNextAsyncAndDispose(moveNexts[i], enumerators[i]); } await Task.WhenAll(cleanup).ConfigureAwait(false); throw; } var moveNextWinner = await Task.WhenAny(moveNexts).ConfigureAwait(false); // // NB: The use of IndexOf is fine. If task N completed by returning a ValueTask<bool> // which is equivalent to the task returned by task M (where M < N), AsTask may // return the same reference (e.g. due to caching of completed Boolean tasks). In // such a case, IndexOf will find task M rather than N in the array, but both have // an equivalent completion state (because they're reference equal). This only leads // to a left-bias in selection of sources, but given Amb's "ambiguous" nature, this // is acceptable. // var winnerIndex = Array.IndexOf(moveNexts, moveNextWinner); var winner = enumerators[winnerIndex]; var loserCleanupTasks = new List <Task>(n - 1); for (var i = n - 1; i >= 0; i--) { if (i != winnerIndex) { individualTokenSources[i].Cancel(); var loserCleanupTask = AwaitMoveNextAsyncAndDispose(moveNexts[i], enumerators[i]); loserCleanupTasks.Add(loserCleanupTask); } } var cleanupLosers = Task.WhenAll(loserCleanupTasks); try { await using (winner.ConfigureAwait(false)) { if (!await moveNextWinner.ConfigureAwait(false)) { yield break; } yield return(winner.Current); while (await winner.MoveNextAsync().ConfigureAwait(false)) { yield return(winner.Current); } } } finally { await cleanupLosers.ConfigureAwait(false); } } }
public static IAsyncEnumerable <TSource> Merge <TSource>(params IAsyncEnumerable <TSource>[] sources) { if (sources == null) { throw Error.ArgumentNull(nameof(sources)); } #if USE_FAIR_AND_CHEAPER_MERGE // // This new implementation of Merge differs from the original one in a few ways: // // - It's cheaper because: // - no conversion from ValueTask<bool> to Task<bool> takes place using AsTask, // - we don't instantiate Task.WhenAny tasks for each iteration. // - It's fairer because: // - the MoveNextAsync tasks are awaited concurently, but completions are queued, // instead of awaiting a new WhenAny task where "left" sources have preferential // treatment over "right" sources. // return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { var count = sources.Length; var enumerators = new IAsyncEnumerator <TSource> [count]; var moveNextTasks = new ValueTask <bool> [count]; try { for (var i = 0; i < count; i++) { IAsyncEnumerator <TSource> enumerator = sources[i].GetAsyncEnumerator(cancellationToken); enumerators[i] = enumerator; // REVIEW: This follows the lead of the original implementation where we kick off MoveNextAsync // operations immediately. An alternative would be to do this in a separate stage, thus // preventing concurrency across MoveNextAsync and GetAsyncEnumerator calls and avoiding // any MoveNextAsync calls before all enumerators are acquired (or an exception has // occurred doing so). moveNextTasks[i] = enumerator.MoveNextAsync(); } var whenAny = TaskExt.WhenAny(moveNextTasks); int active = count; while (active > 0) { int index = await whenAny; IAsyncEnumerator <TSource> enumerator = enumerators[index]; ValueTask <bool> moveNextTask = moveNextTasks[index]; if (!await moveNextTask.ConfigureAwait(false)) { // // Replace the task in our array by a completed task to make finally logic easier. Note that // the WhenAnyValueTask object has a reference to our array (i.e. no copy is made), so this // gets rid of any resources the original task may have held onto. However, we *don't* call // whenAny.Replace to set this value, because it'd attach an awaiter to the already completed // task, causing spurious wake-ups when awaiting whenAny. // moveNextTasks[index] = new ValueTask <bool>(); // REVIEW: The original implementation did not dispose eagerly, which could lead to resource // leaks when merged with other long-running sequences. enumerators[index] = null; // NB: Avoids attempt at double dispose in finally if disposing fails. await enumerator.DisposeAsync().ConfigureAwait(false); active--; } else { TSource item = enumerator.Current; // // Replace the task using whenAny.Replace, which will write it to the moveNextTasks array, and // will start awaiting the task. Note we don't have to write to moveNextTasks ourselves because // the whenAny object has a reference to it (i.e. no copy is made). // whenAny.Replace(index, enumerator.MoveNextAsync()); yield return(item); } } } finally { // REVIEW: The original implementation performs a concurrent dispose, which seems undesirable given the // additional uncontrollable source of concurrency and the sequential resource acquisition. In // this modern implementation, we release resources in opposite order as we acquired them, thus // guaranteeing determinism (and mimicking a series of nested `await using` statements). // REVIEW: If we decide to phase GetAsyncEnumerator and the initial MoveNextAsync calls at the start of // the operator implementation, we should make this symmetric and first await all in flight // MoveNextAsync operations, prior to disposing the enumerators. var errors = default(List <Exception>); for (var i = count - 1; i >= 0; i--) { ValueTask <bool> moveNextTask = moveNextTasks[i]; IAsyncEnumerator <TSource> enumerator = enumerators[i]; try { try { // // Await the task to ensure outstanding work is completed prior to performing a dispose // operation. Note that we don't have to do anything special for tasks belonging to // enumerators that have finished; we swapped in a placeholder completed task. // // REVIEW: This adds an additional continuation to all of the pending tasks (note that // whenAny also has registered one). The whenAny object will be collectible // after all of these complete. Alternatively, we could drain via whenAny, by // awaiting it until the active count drops to 0. This saves on attaching the // additional continuations, but we need to decide on order of dispose. Right // now, we dispose in opposite order of acquiring the enumerators, with the // exception of enumerators that were disposed eagerly upon early completion. // Should we care about the dispose order at all? _ = await moveNextTask.ConfigureAwait(false); } finally { if (enumerator != null) { await enumerator.DisposeAsync().ConfigureAwait(false); } } } catch (Exception ex) { if (errors == null) { errors = new List <Exception>(); } errors.Add(ex); } } // NB: If we had any errors during cleaning (and awaiting pending operations), we throw these exceptions // instead of the original exception that may have led to running the finally block. This is similar // to throwing from any finally block (except that we catch all exceptions to ensure cleanup of all // concurrent sequences being merged). if (errors != null) { throw new AggregateException(errors); } } } #elif USE_ASYNC_ITERATOR return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { var count = sources.Length; var enumerators = new IAsyncEnumerator <TSource> [count]; var moveNextTasks = new Task <bool> [count]; try { for (var i = 0; i < count; i++) { var enumerator = sources[i].GetAsyncEnumerator(cancellationToken); enumerators[i] = enumerator; // REVIEW: This follows the lead of the original implementation where we kick off MoveNextAsync // operations immediately. An alternative would be to do this in a separate stage, thus // preventing concurrency across MoveNextAsync and GetAsyncEnumerator calls and avoiding // any MoveNextAsync calls before all enumerators are acquired (or an exception has // occurred doing so). moveNextTasks[i] = enumerator.MoveNextAsync().AsTask(); } var active = count; while (active > 0) { // REVIEW: Performance of WhenAny may be an issue when called repeatedly like this. We should // measure and could consider operating directly on the ValueTask<bool> objects, thus // also preventing the Task<bool> allocations from AsTask. var moveNextTask = await Task.WhenAny(moveNextTasks).ConfigureAwait(false); // REVIEW: This seems wrong. AsTask can return the original Task<bool> (if the ValueTask<bool> // is wrapping one) or return a singleton instance for true and false, at which point // the use of IndexOf may pick an element closer to the start of the array because of // reference equality checks and aliasing effects. See GetTaskForResult in the BCL. var index = Array.IndexOf(moveNextTasks, moveNextTask); var enumerator = enumerators[index]; if (!await moveNextTask.ConfigureAwait(false)) { moveNextTasks[index] = TaskExt.Never; // REVIEW: The original implementation did not dispose eagerly, which could lead to resource // leaks when merged with other long-running sequences. enumerators[index] = null; // NB: Avoids attempt at double dispose in finally if disposing fails. await enumerator.DisposeAsync().ConfigureAwait(false); active--; } else { var item = enumerator.Current; moveNextTasks[index] = enumerator.MoveNextAsync().AsTask(); yield return(item); } } } finally { // REVIEW: The original implementation performs a concurrent dispose, which seems undesirable given the // additional uncontrollable source of concurrency and the sequential resource acquisition. In // this modern implementation, we release resources in opposite order as we acquired them, thus // guaranteeing determinism (and mimicking a series of nested `await using` statements). // REVIEW: If we decide to phase GetAsyncEnumerator and the initial MoveNextAsync calls at the start of // the operator implementation, we should make this symmetric and first await all in flight // MoveNextAsync operations, prior to disposing the enumerators. var errors = default(List <Exception>); for (var i = count - 1; i >= 0; i--) { var moveNextTask = moveNextTasks[i]; var enumerator = enumerators[i]; try { try { if (moveNextTask != null && moveNextTask != TaskExt.Never) { _ = await moveNextTask.ConfigureAwait(false); } } finally { if (enumerator != null) { await enumerator.DisposeAsync().ConfigureAwait(false); } } } catch (Exception ex) { if (errors == null) { errors = new List <Exception>(); } errors.Add(ex); } } // NB: If we had any errors during cleaning (and awaiting pending operations), we throw these exceptions // instead of the original exception that may have led to running the finally block. This is similar // to throwing from any finally block (except that we catch all exceptions to ensure cleanup of all // concurrent sequences being merged). if (errors != null) { throw new AggregateException(errors); } } } #else return(new MergeAsyncIterator <TSource>(sources)); #endif }
public static IAsyncEnumerable <TSource> Amb <TSource>(this IAsyncEnumerable <TSource> first, IAsyncEnumerable <TSource> second) { if (first == null) { throw Error.ArgumentNull(nameof(first)); } if (second == null) { throw Error.ArgumentNull(nameof(second)); } return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { IAsyncEnumerator <TSource>?firstEnumerator = null; IAsyncEnumerator <TSource>?secondEnumerator = null; Task <bool>?firstMoveNext = null; Task <bool>?secondMoveNext = null; // // We need separate tokens for each source so that the non-winner can get disposed and unblocked // i.e., see Never() // var firstCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var secondCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); try { // // REVIEW: We start both sequences unconditionally. An alternative implementation could be to just stick // to the first sequence if we notice it already has a value (or exception) available. This would // be similar to Task.WhenAny behavior (see CommonCWAnyLogic in dotnet/coreclr). We could consider // adding a WhenAny combinator that does exactly that. We can even avoid calling AsTask. // firstEnumerator = first.GetAsyncEnumerator(firstCancelToken.Token); firstMoveNext = firstEnumerator.MoveNextAsync().AsTask(); // // REVIEW: Order of operations has changed here compared to the original, but is now in sync with the N-ary // overload which performs GetAsyncEnumerator/MoveNextAsync in pairs, rather than phased. // secondEnumerator = second.GetAsyncEnumerator(secondCancelToken.Token); secondMoveNext = secondEnumerator.MoveNextAsync().AsTask(); } catch { secondCancelToken.Cancel(); firstCancelToken.Cancel(); // NB: AwaitMoveNextAsyncAndDispose checks for null for both arguments, reducing the need for many null // checks over here. var cleanup = new[] { AwaitMoveNextAsyncAndDispose(secondMoveNext, secondEnumerator), AwaitMoveNextAsyncAndDispose(firstMoveNext, firstEnumerator) }; await Task.WhenAll(cleanup).ConfigureAwait(false); throw; } // // REVIEW: Consider using the WhenAny combinator defined for Merge in TaskExt, which would avoid the need // to convert to Task<bool> prior to calling Task.WhenAny. // var moveNextWinner = await Task.WhenAny(firstMoveNext, secondMoveNext).ConfigureAwait(false); // // REVIEW: An alternative option is to call DisposeAsync on the other and await it, but this has two drawbacks: // // 1. Concurrent DisposeAsync while a MoveNextAsync is in flight. // 2. The winner elected by Amb is blocked to yield results until the loser unblocks. // IAsyncEnumerator <TSource> winner; Task disposeLoser; if (moveNextWinner == firstMoveNext) { winner = firstEnumerator; secondCancelToken.Cancel(); disposeLoser = AwaitMoveNextAsyncAndDispose(secondMoveNext, secondEnumerator); } else { winner = secondEnumerator; firstCancelToken.Cancel(); disposeLoser = AwaitMoveNextAsyncAndDispose(firstMoveNext, firstEnumerator); } try { await using (winner.ConfigureAwait(false)) { if (!await moveNextWinner.ConfigureAwait(false)) { yield break; } yield return(winner.Current); while (await winner.MoveNextAsync().ConfigureAwait(false)) { yield return(winner.Current); } } } finally { // // REVIEW: This behavior differs from the original implementation in that we never discard any in flight // asynchronous operations. If an exception occurs while enumerating the winner, it can be // subsumed by an exception thrown due to cleanup of the loser. Also, if Amb is used to deal with // a potentially long-blocking sequence, this implementation would transfer this blocking behavior // to the resulting sequence. However, it seems never discarding a non-completed task should be a // general design tenet, and fire-and-forget dispose behavior could be added as another "unsafe" // operator, so all such sins are made explicit by the user. Nonetheless, this change is a breaking // change for Ix Async. // await disposeLoser.ConfigureAwait(false); } } }
public static IAsyncEnumerable <TSource> Catch <TSource, TException>(this IAsyncEnumerable <TSource> source, Func <TException, ValueTask <IAsyncEnumerable <TSource> > > handler) where TException : Exception { if (source == null) { throw Error.ArgumentNull(nameof(source)); } if (handler == null) { throw Error.ArgumentNull(nameof(handler)); } #if USE_ASYNC_ITERATOR return(AsyncEnumerable.Create(Core)); async IAsyncEnumerator <TSource> Core(CancellationToken cancellationToken) { // REVIEW: This implementation mirrors the Ix implementation, which does not protect GetEnumerator // using the try statement either. A more trivial implementation would use await foreach // and protect the entire loop using a try statement, with two breaking changes: // // - Also protecting the call to GetAsyncEnumerator by the try statement. // - Invocation of the handler after disposal of the failed first sequence. var err = default(IAsyncEnumerable <TSource>); await using (var e = source.GetConfiguredAsyncEnumerator(cancellationToken, false)) { while (true) { var c = default(TSource); try { if (!await e.MoveNextAsync()) { break; } c = e.Current; } catch (TException ex) { err = await handler(ex).ConfigureAwait(false); break; } yield return(c); } } if (err != null) { await foreach (var item in AsyncEnumerableExtensions.WithCancellation(err, cancellationToken).ConfigureAwait(false)) { yield return(item); } } } #else return(new CatchAsyncIteratorWithTask <TSource, TException>(source, handler)); #endif }