如何转换这个并行.ForEach代码为async/await

本文关键字:代码 ForEach async await 并行 何转换 转换 | 更新日期: 2023-09-27 18:15:41

我很难理解async/await。我正在帮助使用一个现有的代码库,它具有以下代码(简化,为了简洁):

List<BuyerContext> buyerContexts = GetBuyers();
var results = new List<Result>();
Parallel.ForEach(buyerContexts, buyerContext =>
{
    //The following call creates a connection to a remote web server that 
    //can take up to 15 seconds to respond
    var result = Bid(buyerContext);
    if (result != null)
        results.Add(result);
}
foreach (var result in results)
{
  // do some work here that is predicated on the 
  // Parallel.ForEach having completed all of its calls
}

我如何将此代码转换为异步代码,而不是使用async/await并行?我正在遭受一些相当严重的性能问题,我认为这是对多个网络I/O操作使用并行方法的结果。

我自己尝试过几种方法,但我从Visual Studio得到警告,我的代码将同步执行,或者我不能在异步方法之外使用await关键字,所以我确信我只是错过了一些简单的东西。

EDIT #1:我对async/await的替代方案持开放态度。根据我目前的阅读,这似乎是正确的方法。

EDIT #2:此应用程序是Windows服务。它会召集几个"买家",请他们对某一特定数据进行竞标。我需要所有的投标回来之前处理可以继续。

如何转换这个并行.ForEach代码为async/await

"让事情异步化"的关键是从叶子开始。在这种情况下,从您的网络代码开始(未显示),并将您拥有的任何同步调用(例如,WebClient.DownloadString)更改为相应的异步调用(例如,HttpClient.GetStringAsync)。然后await那呼叫。

使用await将强制调用方法为async,并将其返回类型从T更改为Task<T>。在这里,添加Async后缀也是一个好主意,这样您就遵循了众所周知的约定。然后取所有方法的调用者,并将它们更改为使用await,这将要求它们为async,等等。重复,直到你有一个BidAsync方法可以使用。

那么你应该考虑替换你的并行循环;使用Task.WhenAll:

很容易做到这一点
List<BuyerContext> buyerContexts = GetBuyers();
var tasks = buyerContexts.Select(buyerContext => BidAsync(buyerContext));
var results = await Task.WhenAll(tasks);
foreach (var result in results)
{
  ...
}

基本上,为了使用async-await, Bid方法应该使用这个签名而不是当前的签名:

public async Task<Result> BidAsync(BuyerContext buyerContext);

这将允许您在此方法中使用await。现在,每次进行网络呼叫时,基本上都需要await。例如,下面是如何将同步方法的调用和签名修改为异步方法的方法。

//Signature
public string ReceiveStringFromClient();
//Call
string messageFromClient = ReceiveStringFromClient();

//Signature
public Task<string> ReceiveStringFromClientAsync();
//Call
string messageFromClient = await ReceiveStringFromClientAsync();

如果您仍然需要能够对这些方法进行同步调用,我建议创建以"Async"为后缀的新方法。

现在你需要在每一层都这样做,直到你到达你的网络调用,在这一点上,你将能够等待。net的async方法。它们通常具有与同步版本相同的名称,后缀为"Async"。

一旦你完成了所有这些,你就可以在你的主代码中使用它。我会这样做:

List<BuyerContext> buyerContexts = GetBuyers();
var results = new List<Result>();
List<Task> tasks = new List<Task>();
//There really is no need for Parallel.ForEach unless you have hundreds of thousands of requests to make.
//If that's the case, I hope you have a good network interface!
foreach (var buyerContext in buyerContexts)
{
    var task = Task.Run(async () =>
    {
        var result = await BidAsync(buyerContext);        
        if (result != null)
            results.Add(result);
    });
    tasks.Add(task);
}
//Block the current thread until all the calls are completed
Task.WaitAll(tasks);
foreach (var result in results)
{
  // do some work here that is predicated on the 
  // Parallel.ForEach having completed all of its calls
}