我有一个Task
集合,必须并发或顺序运行。
同时:
public Task Foo(CancellationToken token)
{
// ...
IEnumerable<Task> tasks = GetTasks(token);
// ...
return Task.WhenAll(tasks);
}
但我不确定顺序:
public Task Bar(CancellationToken token) // no `async` keyword by design
{
// ...
IEnumerable<Task> tasks = GetTasks(token);
// ...
// (1)
return Task.Run(async () => {
foreach (var task in tasks)
await task.WaitAsync(token);
}, token);
// (2)
return Task.Run(async () => {
foreach (var task in tasks)
await Task.Run(() => task, token);
}, token);
// (3)
foreach (var task in tasks)
yield return Task.Run(() => task, token);
// (?)
}
在c# 10中,哪一种最安全、最现代、最习惯的方式是:1、2还是3?或者也许有更好/更安全的方法?
这段代码大约每秒运行一次,执行大约10个任务——性能不是最重要的,但仍然很重要。(这是在ASP中运行的。. NET核心服务器应用程序)
UPDATE
正如评论中提到的,GetTasks()
的实现很重要。请说明您期望如何实现它,以便上面的调用代码可以依次运行任务。
这在很大程度上取决于GetTasks
是如何实现的。
让我们讨论两个选项:
// Option 1:
IEnumerable<Task> GetTasks(CancellationToken cancellationToken)
{
foreach (var ... in ...)
yield return Task.Run(<some sync method>);
// alternatively
foreach (var ... in ...)
yield return <some real async method>();
}
// Option 2:
IEnumerable<Task> GetTasks(CancellationToken cancellationToken)
{
return (from ... in ...
select Task.Run(<some sync method>)).ToList();
// alternatively
return (from ... in ...
select <some real async method>()).ToList();
}
在Option 1
中,调用者能够决定任务是并发运行还是顺序运行,因为每个启动的任务都是顺序产生的。
在Option 2
中,所有任务当前都是并行运行的,无论你如何等待它们。
让我们假设使用Option 1
(或类似的东西):
在这种情况下,您可以使用:
public Task Bar(CancellationToken token)
{
// ToList collects all Tasks without any await so all are running in parallel
// Maybe ToList is not needed and Task.WhenAll does that for you - I'm not sure about that
return Task.WhenAll(GetTasks(token).ToList());
}
或者您一个接一个地检索任务(顺序):
public async Task Bar(CancellationToken token)
{
// This will await one task after another and so they're running sequential
foreach (var item in GetTasks(token))
await item;
}
我真的不明白为什么Bar
不是async
的设计,因为async
不改变方法签名,对调用者完全透明。但是如果需要的话,可以这样实现:
public Task Bar(CancellationToken token)
{
return Task.Run(Body);
async Task Body()
{
// This will await one task after another and so they're running sequential
foreach (var item in GetTasks(token))
await item;
}
}
parallel-Bar
和sequential-Bar
的主要区别在于异常处理。如果其中一个任务发生异常,并行实现甚至会将它们全部运行,直到它们完成。顺序实现将在第一个异常发生后停止启动新任务。这可以使用try...catch
并缓冲所有任务,并在所有任务完成后引发AggregateException
来修复。
根据你的实现:请记住,WaitAsync
只会停止等待任务,如果CancellationToken请求停止。这不会停止正在运行的任务本身。Task.Run
也一样。如果已经设置了CancellationToken,它将阻止任务被调度,但上下文对象中已经运行的任务不会受到影响。GetTask
需要以一种尊重CancellationToken的方式实现。
没有必要用Task.Run
包装一个已经在运行的任务。
编辑
我搜索了Task.WhenAll
的源代码,看起来它为您并行检索所有任务。如果参数已经是一个数组,也会进行性能优化。因此,所示的并行Bar
的实现应该调用ToArray
而不是ToList
,或者转发IEnumerable<Task>
而不收集所有项,因此Task.WhenAll
将为您完成此操作。
// no ``async`` keyword by design
是一种很难闻的气味;如果没有async
,选项1和选项2将无法编译。
在
static async Task MethodAsync(int i) { WriteLine($"<{i}"); await Task.Delay(100); WriteLine($"{i}>"); }
static IEnumerable<Task> GetTasks()
{
for(int i=0 ; i<5;i++ )
{
yield return MethodAsync(i);
}
}
我们可以使用foreach
:
var tasks = GetTasks();
foreach (var t in tasks) // Please note it is the enumeration which starts the tasks one by one
{
await t;
}
这个打印
<0
0>
<1
1>
<2
2>
<3
3>
<4
4>
为了说明是foreach(而不是await
)启动任务,让我们直接使用枚举数:
var tasks = GetTasks();
var enumerator = tasks.GetEnumerator();
var b = true;
while (b)
{
Console.WriteLine("Moving to next");
b = enumerator.MoveNext();
Console.WriteLine("Moved");
if(b)
{
await enumerator.Current;
Console.WriteLine("After await");
}
}
这个打印:
Moving to next
<0
Moved
0>
After await
Moving to next
<1
Moved
1>
After await
Moving to next
<2
Moved
2>
After await
Moving to next
<3
Moved
3>
After await
Moving to next
<4
Moved
4>
After await
Moving to next
Moved
需要注意的是,GetTasks
是这里的负责人。
使用下面的实现,任务将在GetTasks()返回时已经启动(如果有很多任务,则计划启动)。
static IEnumerable<Task> GetTasks()
{
List<Task> tasks = new ();
for(int i=0 ; i<5;i++ )
{
tasks.Add(MethodAsync(i));
}
return tasks;
}
在这种情况下,foreach只会按顺序等待(这不是很有用)。
var tasks = GetTasks();
foreach (var t in tasks)
{
await t;
}
的结果会像这样:
<0
<1
<2
<3
<4
1>
4>
3>
0>
2>