// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; namespace EpicGames.Core { /// /// Utility functions for manipulating async tasks /// public static class AsyncUtils { /// /// Converts a cancellation token to a waitable task /// /// Cancellation token /// public static Task AsTask(this CancellationToken token) { return Task.Delay(-1, token).ContinueWith(x => { }, TaskScheduler.Default); } /// /// Converts a cancellation token to a waitable task /// /// Cancellation token /// public static Task AsTask(this CancellationToken token) { return Task.Delay(-1, token).ContinueWith(_ => Task.FromCanceled(token), TaskScheduler.Default).Unwrap(); } /// /// Waits for a task to complete, ignoring any cancellation exceptions /// /// Task to wait for public static async Task IgnoreCanceledExceptionsAsync(this Task task) { try { await task.ConfigureAwait(false); } catch (OperationCanceledException) { } } /// /// Returns a task that will be abandoned if a cancellation token is activated. This differs from the normal cancellation pattern in that the task will run to completion, but waiting for it can be cancelled. /// /// Task to wait for /// Cancellation token for the operation /// Wrapped task public static async Task AbandonOnCancelAsync(this Task task, CancellationToken cancellationToken) { if (cancellationToken.CanBeCanceled) { await await Task.WhenAny(task, Task.Delay(-1, cancellationToken)); // Double await to ensure cancellation exception is rethrown if returned } return await task; } /// /// Attempts to get the result of a task, if it has finished /// /// /// /// /// public static bool TryGetResult(this Task task, out T result) { if (task.IsCompleted) { result = task.Result; return true; } else { result = default!; return false; } } /// /// Waits for a time period to elapse or the task to be cancelled, without throwing an cancellation exception /// /// Time to wait /// Cancellation token /// public static Task DelayNoThrow(TimeSpan time, CancellationToken token) { return Task.Delay(time, token).ContinueWith(x => { }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); } /// /// Removes all the complete tasks from a list, allowing each to throw exceptions as necessary /// /// List of tasks to remove tasks from public static void RemoveCompleteTasks(this List tasks) { List exceptions = []; int outIdx = 0; for (int idx = 0; idx < tasks.Count; idx++) { if (tasks[idx].IsCompleted) { AggregateException? exception = tasks[idx].Exception; if (exception != null) { exceptions.AddRange(exception.InnerExceptions); } } else { if (idx != outIdx) { tasks[outIdx] = tasks[idx]; } outIdx++; } } tasks.RemoveRange(outIdx, tasks.Count - outIdx); if (exceptions.Count > 0) { throw new AggregateException(exceptions); } } /// /// Removes all the complete tasks from a list, allowing each to throw exceptions as necessary /// /// List of tasks to remove tasks from /// Return values from the completed tasks public static List RemoveCompleteTasks(this List> tasks) { List results = []; int outIdx = 0; for (int idx = 0; idx < tasks.Count; idx++) { if (tasks[idx].IsCompleted) { results.Add(tasks[idx].Result); } else if (idx != outIdx) { tasks[outIdx++] = tasks[idx]; } } tasks.RemoveRange(outIdx, tasks.Count - outIdx); return results; } /// /// Starts prefetching the next item from an async enumerator while the current one is being processes /// /// Value type /// Sequence to enumerate /// Cancellation token for the operation /// public static async IAsyncEnumerable PrefetchAsync(this IAsyncEnumerable source, [EnumeratorCancellation] CancellationToken cancellationToken) { await using IAsyncEnumerator enumerator = source.GetAsyncEnumerator(cancellationToken); if (await enumerator.MoveNextAsync()) { T value = enumerator.Current; for (; ; ) { cancellationToken.ThrowIfCancellationRequested(); Task task = enumerator.MoveNextAsync().AsTask(); try { yield return value; } finally { await task; // Async state machine throws a NotSupportedException if disposed before awaiting this task } if (!await task) { break; } value = enumerator.Current; } } } /// /// Starts prefetching a number of items from an async enumerator while the current one is being processes /// /// Value type /// Sequence to enumerate /// Number of items to prefetch /// Cancellation token for the operation /// public static IAsyncEnumerable Prefetch(this IAsyncEnumerable source, int count, CancellationToken cancellationToken = default) { if (count == 0) { return source; } else { return Prefetch(source, count - 1, cancellationToken); } } /// /// Waits for a native wait handle to be signaled /// /// Handle to wait for /// Cancellation token for the operation public static Task WaitOneAsync(this WaitHandle handle, CancellationToken cancellationToken = default) => handle.WaitOneAsync(-1, cancellationToken); /// /// Waits for a native wait handle to be signaled /// /// Handle to wait for /// Timeout for the wait /// Cancellation token for the operation public static async Task WaitOneAsync(this WaitHandle handle, int timeoutMs, CancellationToken cancellationToken = default) { TaskCompletionSource completionSource = new TaskCompletionSource(); RegisteredWaitHandle waitHandle = ThreadPool.RegisterWaitForSingleObject(handle, (state, timedOut) => ((TaskCompletionSource)state!).TrySetResult(!timedOut), completionSource, timeoutMs, true); try { using IDisposable registration = cancellationToken.Register(x => ((TaskCompletionSource)x!).SetCanceled(), completionSource); await completionSource.Task; } finally { waitHandle.Unregister(null); } } } }