5

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.

Scott
  • 13,735
  • 20
  • 94
  • 152
  • Can you add the Bid method definition? If it's network related, it would really help to see the network code :) – Philippe Paré Oct 02 '15 at 01:50
  • Sounds like an extremely expensive call to make in a loop. Do you need all of the results up front? Could you perhaps get a few results and only go back for more if, say, a user asks for more ? – Jeffrey Knight Oct 02 '15 at 01:51
  • See http://stackoverflow.com/questions/9290498/how-can-i-limit-parallel-foreach for a simple solution. – beerboy Oct 02 '15 at 01:51
  • The Bid method is has a ton of logic and the actual code to make the network connection is buried within an abstract base class several levels deep. It wouldn't be prudent to post that all here. – Scott Oct 02 '15 at 01:57
  • In my opinion, `async` is more likely to be the solution here. `Parallel.ForEach` is simply making the calls in parallel, the network interface is to blame for slow downs here – Philippe Paré Oct 02 '15 at 01:58
  • I know, that's why I asked for help converting it to async. :) I could really use a code sample for how to convert the above code to async. – Scott Oct 02 '15 at 01:59

2 Answers2

4

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)
{
  ...
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
3

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
}
Philippe Paré
  • 4,279
  • 5
  • 36
  • 56
  • That's true, I changed from single line to variable + add but forgot the second one, fixing! – Philippe Paré Oct 02 '15 at 02:30
  • Thanks! One question: do async methods HAVE to end with Async()? Reason for asking is that the Bid() method contains a call to a method implemented via an interface. If I have to change the method name to Async() I have to update about 50 projects. :) – Scott Oct 02 '15 at 02:39
  • Note unless bidasync is truly asynchronous, this will have the exact same performance as the original. The easiest way to eke out more performance is to increase the thread pool size. – Dax Fohl Oct 02 '15 at 04:27