-1

I am working on some performance optimization of the application therefore started working to make parallel call. But have seen lot of discrepancy in data processing, its fully inconsistent which request will gets rejected (failed). Here below sample code and its result in 4 round of the testing. Trying to make 50 parallel API call and each time some of the request getting failed. Each time you can some of the request getting dropped by Parallel.ForEach and Task

Parallel Processing Analysis on 50 sample API Call

Round-1
Time taken by Parallel.Foreach :00:00:00.2732286  -  Count:50
Time taken by Task             :00:00:00.0025059  -  Count:48
Time taken by Foreach          :00:00:08.3770130  -  Count:50

Round-2
Time taken by Parallel.Foreach :00:00:00.1271151  -  Count:46
Time taken by Task             :00:00:00.0005574  -  Count:50
Time taken by Foreach          :00:00:05.3288707  -  Count:50

Round-3
Time taken by Parallel.Foreach :00:00:00.1224027  -  Count:49
Time taken by Task             :00:00:00.0003799  -  Count:50
Time taken by Foreach          :00:00:05.2718811  -  Count:50

Round-4
Time taken by Parallel.Foreach :00:00:00.1295570  -  Count:49
Time taken by Task             :00:00:00.0004238  -  Count:48
Time taken by Foreach          :00:00:05.2395539  -  Count:50

C# sample Code

async static Task Main(string[] args)
        {
            HttpClient httpClient = new HttpClient();

            var res1 = new List<HttpResponseMessage>();
            var res2 = new List<HttpResponseMessage>();
            var res3 = new List<HttpResponseMessage>();

            var countlist = new List<int>();

            for (int i = 1; i <= 50; i++)
            {
                countlist.Add(i);
            }

            // Parallel foreach
            Stopwatch stopwatch1 = Stopwatch.StartNew();
            Parallel.ForEach(countlist, async (item) =>
             {
                 var data = await httpClient.GetAsync("https://jsonplaceholder.typicode.com/todos/1");
                 res1.Add(data);
             });
            stopwatch1.Stop();

            // Task parallel
            Stopwatch stopwatch2 = Stopwatch.StartNew();

            var listtask = new List<Task>();
            foreach (var item in countlist)
            {
                listtask.Add(Task.Run(async () =>
                {
                    var data = await httpClient.GetAsync("https://jsonplaceholder.typicode.com/todos/1");
                    res2.Add(data);
                }));
            }
            stopwatch2.Stop();


            // Normal foreach
            Stopwatch stopwatch3 = Stopwatch.StartNew();
            foreach (var item in countlist)
            {
                var data = await httpClient.GetAsync("https://jsonplaceholder.typicode.com/todos/1");
                res3.Add(data);
            }
            stopwatch3.Stop();

            Console.WriteLine($"Time taken by Parallel.Foreach :{stopwatch1.Elapsed}  -  Count:{res1.Count}");
            Console.WriteLine($"Time taken by Task             :{stopwatch2.Elapsed}  -  Count:{res2.Count}");
            Console.WriteLine($"Time taken by Foreach          :{stopwatch3.Elapsed}  -  Count:{res3.Count}");

        }

Any help or suggestion would be appreciated, I wanted to use Task based parallel calls since its gives best performance in all

Rahul Shukla
  • 646
  • 6
  • 19
  • 1
    Operations on list are not threadsafe, use a concurrent data type instead, like concurrentbag. In otherwords, items are being "dropped" because when concurrent access happens on the list it might insert 2 items on the same place in the list, and thus you end up missing items. – FuriousCactus May 11 '21 at 11:40
  • 2
    Your `Task` results are not correct. You need to `await Task.WhenAll(listTask)` before you stop stopwatch 2. – sellotape May 11 '21 at 11:44
  • @FuriousCactus Thanks, just tried with ConcurrentBag each time its working perfectly. – Rahul Shukla May 11 '21 at 11:44
  • @sellotape is also correct! OP your measurement of the task example is not correct, you need to await the tasks for completion. Otherwise you stop the stopwatch after the tasks are started, not when they are done. – FuriousCactus May 11 '21 at 11:47
  • @sellotape Thanks for point out that too, I missed in the sample code – Rahul Shukla May 11 '21 at 11:49
  • The `Parallel.ForEach` is not async-friendly. The lambda passed is [`async void`](https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void), a common gotcha. In the next major .NET release there will probably be a new [`Parallel.ForEachAsync`](https://github.com/dotnet/runtime/issues/1946) method that you could use, but for now you can look here: [How to limit the amount of concurrent async I/O operations?](https://stackoverflow.com/questions/10806951/how-to-limit-the-amount-of-concurrent-async-i-o-operations) – Theodor Zoulias May 11 '21 at 17:09

2 Answers2

2

You can't use Parallel with async; Parallel only understands synchronous code.

So, your best bet is to use asynchronous concurrency, i.e., call asynchronous methods and combine the results using Task.WhenAll. Task.Run isn't necessary if the work is asynchronous. Also, it's more natural to return results than to save them into a data structure as a side effect.

Stopwatch stopwatch2 = Stopwatch.StartNew();

var listtask = countlist
    .Select(async () => await httpClient.GetAsync("https://jsonplaceholder.typicode.com/todos/1"))
    .ToList();
res2 = await Task.WhenAll(listtask);

stopwatch2.Stop();
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
0

Posting the final answer with Taskapproach worked for me thanks @FuriousCactus and @sellotape for the providing the correct path. Since list is not thread safe that's why many it leading to issue ConcurrentBag it solves the problem .

async static Task Main(string[] args)
        {
            HttpClient httpClient = new HttpClient();                    
            var res2 = new ConcurrentBag<HttpResponseMessage>();    
            var countlist = new List<int>();    
            for (int i = 1; i <= 50; i++)
                countlist.Add(i);

            // Task parallel
            Stopwatch stopwatch2 = Stopwatch.StartNew();

            var listtask = new List<Task>();
            foreach (var item in countlist)
            {
                listtask.Add(Task.Run(async () =>
                {
                    var data = await httpClient.GetAsync("https://jsonplaceholder.typicode.com/todos/1");
                    res2.Add(data);
                }));
            }
            await Task.WhenAll(listtask);
            stopwatch2.Stop();
         
            Console.WriteLine($"Time taken by Task             :{stopwatch2.Elapsed}  -  Count:{res2.Count}");

        }
Rahul Shukla
  • 646
  • 6
  • 19
  • Hi Rahul. The `Parallel.ForEach` with async delegate is a bug, hence my downvote. The OP is forgiven for including it in their question, because they are asking for suggestions. But including it in an answer is an automatic downvote for me. We should not perpetuate bad practices when answering questions IMHO. – Theodor Zoulias May 11 '21 at 22:35
  • Still tempted to downvote because of the [inappropriate](https://stackoverflow.com/questions/15400133/when-to-use-blockingcollection-and-when-concurrentbag-instead-of-listt/64823123#64823123) use of the `ConcurrentBag`, but OK. :-) – Theodor Zoulias May 12 '21 at 07:36