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