在c# 10中并发或顺序运行task collection



我有一个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>

最新更新