目录
介绍
背景
在ForEach循环中使用Await
使用真正的Task.WhenAll并行循环
代码示例
操作问题的顺序
并发问题
减轻执行问题的并发性和顺序
兴趣点
- 下载源代码 - 7.7 KB
foreach循环,无论如何,C#中的for循环可以通过使用await和Task指令在性能上获得很大的好处。但这并不总是可能的。在这篇文章中,我们分析了C#中foreach循环的await和task.WhenAll使用,什么时候可以使用或者更好使用,以及在某些情况下如何避免。
我们在这里比较了两种不同的方法来使用并行程序和foreach。此外,我们看到了在foreach循环中使用它的潜在问题。
- 在foreach循环内使用await
- 使用真正的Task.WhenAll并行循环
最后,我们还提供了一种可能的解决方案来缓解并发和执行顺序问题。
背景 在ForEach循环中使用Await第一种查看方法位于foreach指令await内部(见图 1)。在这种情况下,当达到await时,thread:
- 可以自由继续,在循环内部,指令一个一个地执行每个任务
- 直到foreach完成,然后指令继续执行
- 到程序结束。
这种方法帮助我们将控制从循环中解放出来以执行其他任务,但方法本身与循环内的任务同步执行。这为您带来以下好处:
- 避免操作中的并发问题
- 为您提供操作以与输入的相同顺序执行的安全性
但是,由于操作是在循环内部以同步的形式执行的,所以该方法的性能不是很好。
使用真正的Task.WhenAll并行循环第二个选项提供了更好的性能:我们可以创建一个任务列表并在循环完成后使用Task.WhenAll,在这种情况下,循环内的任务并行执行,执行时间大大减少。
图中用绿色箭头说明了过程如何在循环内启动所有任务而不等待结果,然后线程释放await指令中的控制并等待所有任务完成。然后执行过程的最后一条指令,并返回结果。如您所见,此选项确实提高了性能,因为它在循环内创建了任务的真正并行执行,减少了消耗更多任务所消耗时间的执行时间,而不是执行时间的总和所有的任务。但是这种方法有两个更大的局限性。他们是:
- 循环中的每个任务之间不能有优先关系。
- 每个任务中的数据不能有并发问题。
让我们详细解释一下:
首先,任务之间的优先关系在于操作需要严格的执行顺序。请记住,当您并行运行此任务时,您无法知道任务何时开始或何时结束,这是不可预测的,如果任务需要执行顺序,则无法保证。在这种情况下,没有其他机会牺牲性能并使用await。
您可以使用随附的代码来测试所有这些条件。
代码示例- 下载示例
此处说明了需要顺序的典型任务:
private async Task DependenTaskAsync(string status)
{
await Task.Delay(1000);
if (status == "low")
{
this.Status.Remove("low");
return;
}
if (status == "medium")
{
if (this.Status.Contains("low"))
{
throw new Exception("Low must not exists when you try to remove medium");
}
this.Status.Remove("medium");
return;
}
if (status == "high")
{
if (this.Status.Contains("low"))
{
throw new Exception("Low must not exists when you try to remove medium");
}
if (this.Status.Contains("medium"))
{
throw new Exception("Medium must not exists when you try to remove height");
}
this.Status.Remove("high");
}
}
在此任务中,您需要将列表中的值从低到高删除,如果忽略抛出异常的顺序,您可以在附加代码中运行此代码。这是一个学校的例子,但现实生活中的许多任务之间存在依赖关系,并且需要一定的操作顺序。
并发问题如果您的操作需要执行隔离,那么您遇到的问题似乎难以理解,但一个简单的示例可以帮助您。例如,请参见以下代码:
private async Task ConcurrencyAffectedTaskAsync(int val)
{
await Task.Delay(1000); // Similar other processing
Total = Total + val;
}
如果你在带有Task.Await的foreach中使用它,这个简单的代码会失败(参见本文所附的示例代码),这是因为Total它是类的一个global变量,你不能在某个时刻控制它,total的值被其他线程改变了,结果可以完全不可预测。如果您运行所附的示例,您可以看到如果您多次运行此代码会得到多少不同的结果。
减轻执行问题的并发性和顺序在某些情况下,您可以缓解此问题,使用C#中的指令强制程序每次仅允许唯一线程在确定的代码段中运行。这可能非常有用,但也有局限性。
如果您不知道并发问题在哪里,或者您需要锁定的代码段占用了总执行时间的很大一部分,那么您不会获得那么多性能,并且您会创建一个更复杂的代码。在这种情况下,最好在循环内使用普通运await算符。
我们的并发示例是使用信号量的一个很好的候选,它是一条指令并且大部分延迟都在这条指令之外,那么就可以了。那么在这种情况下,我们可以创建一个很小的信号量,并将这段代码的执行限制在一个线程中,并在没有并发问题的情况下获得超强的性能。
您可以声明一个信号量:
public class ForEachConcurrentDependency
{
public int Total { get; set;}
public SemaphoreSlim semaphore;
public ForEachConcurrentDependency()
{
this.Total = 0;
semaphore =new SemaphoreSlim(1, 1);
}
// rest of the class...
}
并使用它,观察代码中仅限于一个线程的部分只是处理时间的一小部分。
private async Task ConcurrencyNoAffectedTaskAsync(int val)
{
await Task.Delay(1000); // Similar other processing
await semaphore.WaitAsync();
try
{
Total = Total + val;
}
finally
{
semaphore.Release();
}
}
- 当要执行的任务有数据并发或有严格的执行顺序时在foreach中使用AWAIT。
- 如果SemaphoreSlim由于锁定过程时间很长,解决方案没有给您带来显着的性能提升,请在FOREACH中使用AWAIT。
- 在 foreach之外使用TASK.WHENALL:只有任务是独立的,没有任何特定的执行顺序。这使我们能够使用并行编程的全部功能并获得更好的性能。
- 您可以在视频中看到对本文代码的解释:C# Pill 11 using Async Method...
https://www.codeproject.com/Articles/5316619/Using-Asynchrony-Methods-in-Foreach-Sentences