Scott
Scott

Reputation: 13921

How to convert this Parallel.ForEach code to async/await

I'm having some trouble getting my head around async/await. I'm helping with an existing code base that has the following code (simplified, for brevity):

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
}

How can i convert this code to asynchronous code instead of parallel using async/await? I'm suffering from some pretty severe performance issues that I believe are a result of using a parallel approach to multiple network I/O operations.

I've tried several approaches myself but I'm getting warnings from Visual Studio that my code will execute synchronously or that I can't use await keywords outside of an async method so I'm sure I'm just missing something simple.

EDIT #1: I'm open to alternatives to async/await as well. That just seems to be the proper approach based on my reading so far.

EDIT #2: This application is a Windows Service. It calls out to several "buyers" to ask them to bid on a particular piece of data. I need ALL of the bids back before processing can continue.

Upvotes: 5

Views: 2073

Answers (2)

Stephen Cleary
Stephen Cleary

Reputation: 456342

The key to "making things async" is to start at the leaves. In this case, start in your network code (not shown), and change whatever synchronous call you have (e.g., WebClient.DownloadString) to the corresponding asynchronous call (e.g., HttpClient.GetStringAsync). Then await that call.

Using await will force the calling method to be async, and change its return type from T to Task<T>. It is also a good idea at this point to add the Async suffix so you're following the well-known convention. Then take all of that method's callers and change them to use await as well, which will then require them to be async, etc. Repeat until you have a BidAsync method to use.

Then you should look at replacing your parallel loop; this is pretty easy to do with Task.WhenAll:

List<BuyerContext> buyerContexts = GetBuyers();
var tasks = buyerContexts.Select(buyerContext => BidAsync(buyerContext));
var results = await Task.WhenAll(tasks);

foreach (var result in results)
{
  ...
}

Upvotes: 4

Philippe Par&#233;
Philippe Par&#233;

Reputation: 4408

Basically, to make use of async-await, the Bid method should have this signature instead of the current one:

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

This will allow you to use await in this method. Now, every time you make a network call, you basically need to await it. For example, here's how to modify the call and signature of a synchronous method to an asynchronous one.

Before

//Signature
public string ReceiveStringFromClient();

//Call
string messageFromClient = ReceiveStringFromClient();

After

//Signature
public Task<string> ReceiveStringFromClientAsync();

//Call
string messageFromClient = await ReceiveStringFromClientAsync();

If you still need to be able to make synchronous calls to these methods, I would recommend creating new ones suffixed with "Async".

Now you need to do this on every level until you reach your network calls, at which point you'll be able to await .Net's async methods. They normally have the same name as their synchronous version, suffixed with "Async".

Once you've done all that, you can make use of this in your main code. I would do something along these lines:

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
}

Upvotes: 3

Related Questions