简体   繁体   中英

Asynchronous tasks parallel execution

I am playing around the parallel execution of tasks in .Net. I have implemented function below which executes list of tasks in parallel by using Task.WhenAll. I also have found that there are two options I can use to add tasks in the list. The option 1 is to use Task.Run and pass Func delegate. The option 2 is to add the result of the invoked Func delegate.

So my questions are:

  1. Task.Run (Option 1) takes additional threads from thread pool and execute tasks in them by passing them to Task.WhenAll. So the question is does Task.WhenAll run each task in the list asynchronously so the used threads are taken from and passed back to thread pool or all taken threads are blocked until execution is completed (or an exception raised)?
  2. Does it make any difference if I call Task.Run passing synchronous (non-awaitable) or asynchronous (awaitable) delegates?
  3. In the option 2 - theoretically no additional threads taken from thread pool to execute Tasks in the list. However, the tasks are executed concurrently. Does Task.WhenAll creates threads internally or all the tasks are executed in a single thread created by Task.WhenAll? And how SemaphoreSlim affects concurrent tasks?

What do you think is the best approach to deal with asynchronous parallel tasks?

    public static async Task<IEnumerable<TResult>> ExecTasksInParallelAsync<TSource, TResult>(IEnumerable<TSource> source, Func<TSource, Task<TResult>> task, int minDegreeOfParallelism = 1, int maxDegreeOfParallelism = 1)
    {
        var allTasks = new List<Task<TResult>>();

        using (var throttler = new SemaphoreSlim(minDegreeOfParallelism, maxDegreeOfParallelism))
        {
            foreach (var element in source)
            {
                // do an async wait until we can schedule again
                await throttler.WaitAsync();

                Func<Task<TResult>> func = async () =>
                {
                    try
                    {
                        return await task(element);
                    }
                    finally
                    {
                        throttler.Release();
                    }
                };

                //Option 1
                allTasks.Add(Task.Run(func));
                //Option 2
                allTasks.Add(func.Invoke());
            }

            return await Task.WhenAll(allTasks);
        }
    }

The function above is executed as

  [HttpGet()]
    public async Task<IEnumerable<string>> Get()
    {using (var client = new HttpClient())
        {
            var source = Enumerable.Range(1, 1000).Select(x => "https://dog.ceo/api/breeds/list/all");
            var result = await Class1.ExecTasksInParallelAsync(
                source, async (x) =>
                {
                    var responseMessage = await client.GetAsync(x);

                    return await responseMessage.Content.ReadAsStringAsync();
                }, 100, 200);

            return result;
        }

}

Option 2 tested better

I ran a few tests using your code and determined that option 2 is roughly 50 times faster than option 1, on my machine at least. However, using PLINQ was even 10 times faster than option 2.

Option 3, PLINQ, is even faster

You could replace that whole mess with a single line of PLINQ :

return source.AsParallel().WithDegreeOfParallelism(maxDegreeOfParallelism)
    .Select( s => task(s).GetAwaiter().GetResult() );

Oops... option 4

Turns out my prior solution would reduce parallelism if the task was actually async (I had been testing with a dummy synchronous function). This solution fixes the problem:

var tasks = source.AsParallel()
    .WithDegreeOfParallelism(maxDegreeOfParallelism)
    .Select( s => task(s) );
await Task.WhenAll(tasks);
return tasks.Select( t => t.Result );

I ran this on my laptop with 10,000 iterations. I did three runs to ensure that there wasn't a priming effect. Results:

Run 1
Option 1: Duration: 13727ms
Option 2: Duration: 303ms
Option 3 :Duration: 39ms
Run 2
Option 1: Duration: 13586ms
Option 2: Duration: 287ms
Option 3 :Duration: 28ms
Run 3
Option 1: Duration: 13580ms
Option 2: Duration: 316ms
Option 3 :Duration: 32ms

You can try it on DotNetFiddle but you'll have to use much shorter runs to stay within quota.

In addition to allowing very short and powerful code, PLINQ totally kills it for parallel processing, as LINQ uses a functional programming approach , and the functional approach is way better for parallel tasks .

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