可以使用新的 C# 语言功能来清理 Task.WhenAll 语法吗?
Can new C# language features be used to to clean-up Task.WhenAll syntax?
有了 "async everywhere",触发多个异构操作的能力变得越来越频繁。当前的 Task.WhenAll
方法 return 将其结果作为一个数组,并要求所有任务都 return 同一类型的对象,这使得它的使用有点笨拙。我 喜欢 能够写...
var (i, s, ...) = await AsyncExtensions.WhenAll(
GetAnIntFromARemoteServiceAsync(),
GetAStringFromARemoteServiceAsync(),
... arbitrary list of tasks
);
Console.WriteLine($"Generated int {i} and string {s} ... and other things");
我能想出的最好的实现是
public static class AsyncExtensions
{
public static async Task<(TA , TB )> WhenAll<TA, TB>(Task<TA> operation1, Task<TB> operation2)
{
return (await operation1, await operation2;
}
}
这样做的缺点是我需要实现最多 N 个参数的单独方法。根据 this answer 这只是使用泛型的限制。此实现还有一个限制,即无法支持 void-returning 任务,但这不是一个问题。
我的问题是:是否有任何即将推出的语言功能允许对此采取更简洁的方法?
有一个open feature-request for this on the dotnet/csharplang repository。
该问题还提到了另一个开放的功能请求,tuple splatting which could help, to some extent. How, is explained here。
这两个问题目前都被标记为 [讨论] 和 [功能请求],并且已经“闲置”了一年(2017 年 5 月 - 2018 年 5 月)。
因此我推断答案(目前)是“否”。
采用扩展方式 Joseph Musser 确实写了很多这些供我们复制和粘贴:https://gist.github.com/jnm2/3660db29457d391a34151f764bfe6ef7
从 .NET 6 开始,标准库中没有 API 允许 await
多个异构任务,并在值元组中获取它们的结果。
我想指出的是,您在问题中展示的实现是不正确的。
// Incorrect
public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
return (await task1, await task2);
}
这不是 WhenAll
。这是WhenAllIfSuccessful_Or_WhenFirstFails
。如果 task1
失败,错误将立即传播,并且 task2
将成为 fire-and-forget 任务。在某些情况下,这可能正是您想要的。但通常您不想忘记您的任务,让它们 运行 在后台不被观察到。在继续下一步工作之前,您希望等待所有这些完成。这是实现 WhenAll
方法的更好方法:
// Good enough
public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
await Task.WhenAll(task1, task2).ConfigureAwait(false);
return (task1.Result, task2.Result);
}
这将等待两个任务完成,如果失败,它将传播第一个失败任务的错误(参数列表中的第一个,不是按时间顺序排列)。在大多数情况下,这完全没问题。但是,如果您发现自己处于需要传播所有异常的情况下,它就会变得棘手。下面是我所知道的最短的实现,它精确地模仿了原生 Task.WhenAll
:
的行为
// Best
public static Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
return Task.WhenAll(task1, task2).ContinueWith(t =>
{
if (t.IsCanceled)
{
// Propagate the correct token
CancellationToken ct = default;
try { t.GetAwaiter().GetResult(); }
catch (OperationCanceledException oce) { ct = oce.CancellationToken; }
return Task.FromCanceled<(T1, T2)>(ct);
}
if (t.IsFaulted)
{
var tcs = new TaskCompletionSource<(T1, T2)>();
tcs.SetException(t.Exception.InnerExceptions);
return tcs.Task;
}
return Task.FromResult((task1.Result, task2.Result));
}, default, TaskContinuationOptions.ExecuteSynchronously |
TaskContinuationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();
}
有了 "async everywhere",触发多个异构操作的能力变得越来越频繁。当前的 Task.WhenAll
方法 return 将其结果作为一个数组,并要求所有任务都 return 同一类型的对象,这使得它的使用有点笨拙。我 喜欢 能够写...
var (i, s, ...) = await AsyncExtensions.WhenAll(
GetAnIntFromARemoteServiceAsync(),
GetAStringFromARemoteServiceAsync(),
... arbitrary list of tasks
);
Console.WriteLine($"Generated int {i} and string {s} ... and other things");
我能想出的最好的实现是
public static class AsyncExtensions
{
public static async Task<(TA , TB )> WhenAll<TA, TB>(Task<TA> operation1, Task<TB> operation2)
{
return (await operation1, await operation2;
}
}
这样做的缺点是我需要实现最多 N 个参数的单独方法。根据 this answer 这只是使用泛型的限制。此实现还有一个限制,即无法支持 void-returning 任务,但这不是一个问题。
我的问题是:是否有任何即将推出的语言功能允许对此采取更简洁的方法?
有一个open feature-request for this on the dotnet/csharplang repository。
该问题还提到了另一个开放的功能请求,tuple splatting which could help, to some extent. How, is explained here。
这两个问题目前都被标记为 [讨论] 和 [功能请求],并且已经“闲置”了一年(2017 年 5 月 - 2018 年 5 月)。
因此我推断答案(目前)是“否”。
采用扩展方式 Joseph Musser 确实写了很多这些供我们复制和粘贴:https://gist.github.com/jnm2/3660db29457d391a34151f764bfe6ef7
从 .NET 6 开始,标准库中没有 API 允许 await
多个异构任务,并在值元组中获取它们的结果。
我想指出的是,您在问题中展示的实现是不正确的。
// Incorrect
public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
return (await task1, await task2);
}
这不是 WhenAll
。这是WhenAllIfSuccessful_Or_WhenFirstFails
。如果 task1
失败,错误将立即传播,并且 task2
将成为 fire-and-forget 任务。在某些情况下,这可能正是您想要的。但通常您不想忘记您的任务,让它们 运行 在后台不被观察到。在继续下一步工作之前,您希望等待所有这些完成。这是实现 WhenAll
方法的更好方法:
// Good enough
public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
await Task.WhenAll(task1, task2).ConfigureAwait(false);
return (task1.Result, task2.Result);
}
这将等待两个任务完成,如果失败,它将传播第一个失败任务的错误(参数列表中的第一个,不是按时间顺序排列)。在大多数情况下,这完全没问题。但是,如果您发现自己处于需要传播所有异常的情况下,它就会变得棘手。下面是我所知道的最短的实现,它精确地模仿了原生 Task.WhenAll
:
// Best
public static Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
return Task.WhenAll(task1, task2).ContinueWith(t =>
{
if (t.IsCanceled)
{
// Propagate the correct token
CancellationToken ct = default;
try { t.GetAwaiter().GetResult(); }
catch (OperationCanceledException oce) { ct = oce.CancellationToken; }
return Task.FromCanceled<(T1, T2)>(ct);
}
if (t.IsFaulted)
{
var tcs = new TaskCompletionSource<(T1, T2)>();
tcs.SetException(t.Exception.InnerExceptions);
return tcs.Task;
}
return Task.FromResult((task1.Result, task2.Result));
}, default, TaskContinuationOptions.ExecuteSynchronously |
TaskContinuationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();
}