简体   繁体   中英

Running Multiple Async Tasks - Faster results Chaining vs Parallel?

I have 7 different API calls that I want to make at the same time, and want the UI to be updated when each has been completed.

I have been fiddling with two different ways of doing this, chaining the requests, and shooting all requests off at the same time (in parallel).

Both seem to work but for some reason my Parallel tasks take significantly longer than when I chain them.

I am new to TPL / Parallelism so it may be my code that is incorrect, but wouldn't chaining the requests take longer since each would have to finish before the next started? Rather than in Parallel they all go out at once so you only have to wait for the slowest?

Please let me know if you see faults in my logic or code. I am happy with the response time I am getting but I do not understand why.

My "chaining" code:

        await (Task.Run(() => WindLookup_DoWork()).
            ContinueWith((t) => WindLookup_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()).
            ContinueWith((t) => OFAC_DoWork()).ContinueWith((t) => OFAC_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()).
            ContinueWith((t) => BCEGS_DoWork()).ContinueWith((t) => BCEGS_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()).
            ContinueWith((t) => BOPTerritory_DoWork()).ContinueWith((t) => BOPTerritory_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()).
            ContinueWith((t) => TerrorismTerritory_DoWork()).ContinueWith((t) => TerrorismTerritory_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()).
            ContinueWith((t) => ProtectionClass_DoWork()).ContinueWith((t) => ProtectionClass_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()).
            ContinueWith((t) => AddressValidation_DoWork()).ContinueWith((t) => AddressValidation_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));

My "Parallel" code:

        List<Task> taskList = new List<Task>();
        taskList.Add(Task.Run(() => WindLookup_DoWork()).
            ContinueWith((t) => WindLookup_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));
        taskList.Add(Task.Run(() => BCEGS_DoWork()).
            ContinueWith((t) => BCEGS_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));
        taskList.Add(Task.Run(() => BOPTerritory_DoWork()).
            ContinueWith((t) => BOPTerritory_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));
        taskList.Add(Task.Run(() => TerrorismTerritory_DoWork()).
            ContinueWith((t) => TerrorismTerritory_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));
        taskList.Add(Task.Run(() => ProtectionClass_DoWork()).
            ContinueWith((t) => ProtectionClass_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));
        taskList.Add(Task.Run(() => OFAC_DoWork()).
            ContinueWith((t) => OFAC_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));
        taskList.Add(Task.Run(() => AddressValidation_DoWork()).
            ContinueWith((t) => AddressValidation_ProcessResults(), TaskScheduler.FromCurrentSynchronizationContext()));

        await Task.WhenAll(taskList.ToArray());

I've basically converted my old Background Worker code, which is why there are DoWork methods, and "callback" methods that update the UI.

The DoWork methods call a POST method to an API, and the process results simply populates a text area with the response xml.

I have been fiddling with two different ways of doing this, chaining the requests, and shooting all requests off at the same time (in parallel).

It's important to distinguish between concurrency and parallelism . In your case, you just want to do them all at the same time, which is a form of concurrency . Parallelism is a more specific technique that uses multiple threads to achieve concurrency, which is appropriate for CPU-bound work. In your case, however, the work is I/O-bound (API requests), and in that situation a more appropriate form of concurrency would be asynchrony. Asynchrony is a form of concurrency without using threads.

Both seem to work but for some reason my Parallel tasks take significantly longer than when I chain them.

I'm not sure why they would be significantly longer, but one common problem is that the number of simultaneous requests are being limited, either on the client side ( ServicePointManager.DefaultConnectionLimit ), or on the server side (see, eg, Concurrent Requests and Session State ).

Please let me know if you see faults in my logic or code.

Unfortunately, TPL is rather hard to learn from reference documentation or IntelliSense, because there are so many methods and types that should only be used in very specific situations. In particular, don't use ContinueWith in your scenario; it has the same problems StartNew does (both links are to my blog).

A better (more reliable and easier to maintain) approach is to introduce some helper methods:

async Task WindLookupAsync()
{
  await Task.Run(() => WindLookup_DoWork());
  WindLookup_ProcessResults();
}
// etc. for the others

// Calling code (concurrent):
await Task.WhenAll(
    WindLookupAsync(),
    BCEGSAsync(),
    BOPTerritoryAsync(),
    TerrorismTerritoryAsync(),
    ProtectionClassAsync(),
    OFACAsync(),
    AddressValidationAsync()
);

// Calling code (serial):
await WindLookupAsync();
await BCEGSAsync();
await BOPTerritoryAsync();
await TerrorismTerritoryAsync();
await ProtectionClassAsync();
await OFACAsync();
await AddressValidationAsync();

With the refactored code, there's no need for ContinueWith or explicit TaskScheduler s.

However, each request is still burning a threadpool thread for each request. If this is a desktop app, it's not the end of the world, but it's not using the best solution. As I mentioned at the beginning of this answer, a more appropriate fit for this problem would be asynchrony rather than parallelism .

To make the code asynchronous, you should first start with your POST API call, and change that to use the asynchronous version and call it with await . (Side note: WebClient does have asynchronous methods, but consider changing to HttpClient , which fits async a bit more naturally). As soon as you call that POST API with await , this will require your _DoWork methods to become asynchronous (and return Task instead of void ). At that point, you can change the helper methods above to await those methods directly rather than using Task.Run , eg:

async Task WindLookupAsync()
{
  await WindLookup_DoWork();
  WindLookup_ProcessResults();
}

The calling code (both concurrent and serial versions) remains the same.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM