1

One question about parallel programming.

I need to POST 2 APIs (one after another) to get a Order list. First one is for getting token..

After I got the list, I need to POST 3 APIs (one after another) to integrate these Orders.

These 3 APIs don't accept arrays, so I need to send one by one. I cannot send batch data. With 1 thread it only integrates 10 orders in 1 minute. I need more performance. How can I run foreach part in parallel?

using System.Net.Http;
using System.Text;
using System.Text.Json;

namespace Order_Integrator
{
    public class Program
    {

        static readonly HttpClient client = new HttpClient();

        static async Task Main()
        {

            //Auth
            var connectResponse = await client.PostAsync(connectUrl, connectContent);
            var connectResponseString = await connectResponse.Content.ReadAsStringAsync();
            var connect = JsonSerializer.Deserialize<connectResponse>(connectResponseString);
            
            var token = connect.Token;
            //Get Order List
            var orderResponse = await client.PostAsync(orderUrl, orderContent);
            var orderResponseString = await orderResponse.Content.ReadAsStringAsync();
            var orders = JsonSerializer.Deserialize<orderResponse>(orderResponseString);

            foreach (var order in orders)
            {
            
                //Get Order Details
                //Generate getOrderDetailsContent with order 
                var getOrderDetailsResponse = await client.PostAsync(getOrderDetailsUrl, getOrderDetailsContent);
                var getOrderDetailsResponseString = await getOrderDetailsResponse.Content.ReadAsStringAsync();
                var getOrderDetails = JsonSerializer.Deserialize<getOrderDetailsResponse>(getOrderDetailsResponseString);

                //Create Order
                //Generate createOrderContent with GetOrderDetails
                var createOrderResponse = await client.PostAsync(createOrderUrl, createOrderContent);
                var createOrderResponseString = await createOrderResponse.Content.ReadAsStringAsync();
                var createOrder = JsonSerializer.Deserialize<createOrderResponse>(createOrderResponseString);

                //Create Log
                //Generate createLogContent with CreateOrderResponse
                var createLogResponse = await client.PostAsync(createLogUrl, createLogContent);
                var createLogResponseString = await createLogResponse.Content.ReadAsStringAsync();
                var createLog = JsonSerializer.Deserialize<createLogResponse>(createLogResponseString);
            }
        }
    }
}

2 Answers2

1

You can use Parallel.ForEachAsync

 var options = new ParallelOptions()
{
    MaxDegreeOfParallelism = 20
};
await Parallel.ForEachAsync(orders, options, async (OrderNumber, ct) => {

var getOrderDetailsResponse = await client.PostAsync(getOrderDetailsUrl, getOrderDetailsContent);
    
});
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
ShubhamWagh
  • 565
  • 2
  • 9
  • 2
    `Parallel.ForEach` does not work correctly with async code. Possibly you have meant [`Parallel.ForEachAsync`](https://stackoverflow.com/questions/70249422/using-parallel-foreachasync/70249541#70249541) – Guru Stron Dec 26 '22 at 15:47
  • I tried `Parallel.ForEach(orders, async order =>` But it didn't work. I got an error while adding parameters to client `client.DefaultRequestHeaders.Add("x-csrf-token", xToken);` **already exists** – Ali Karaçay Dec 26 '22 at 15:53
  • This is because the client is `static`. You'll have to handle all conflicts in a `lock` or outside the `Parallel.ForEach` block. – ShubhamWagh Dec 26 '22 at 15:54
  • @Guru Stron `Parallel.ForEachAsync` sounds better. But I've never had problems with the example code that I posted. it works with async code as well. Not sure what the difference is between them. – ShubhamWagh Dec 26 '22 at 15:56
  • 1
    @WandererAboveTheSea the code you've posted will run all the requests at the same time, while the main advantages of using `Parallel.ForEach` is limiting concurrency. – Guru Stron Dec 26 '22 at 15:58
  • Understood the main thread won't wait for all the requests to complete. Even though I'm using await inside `Parallel.ForEach`. – ShubhamWagh Dec 26 '22 at 16:01
  • 2
    Here it is explained why `Parallel.ForEach` with `async` is not working: [Parallel.ForEach and async-await](https://stackoverflow.com/questions/23137393/parallel-foreach-and-async-await). – Theodor Zoulias Dec 26 '22 at 16:08
  • CS0017: Parallel does not contain a definition for ForEachAsync – Ali Karaçay Dec 26 '22 at 16:22
  • It's available in .NET 7 and .NET 6. Which one are you using? – ShubhamWagh Dec 26 '22 at 16:26
  • Which one is used is not important, because that depends on the life cycle of your product, in combination with the life-cycle of [dotnet-core](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core) – Luuk Dec 26 '22 at 16:30
  • it is 4.8 I will upgrade and try again. – Ali Karaçay Dec 26 '22 at 16:31
  • @AliKaraçay you can find [here](https://stackoverflow.com/questions/11564506/nesting-await-in-parallel-foreach/65251949#65251949) a custom `Parallel.ForEachAsync` implementation that is compatible with the .NET Framework. But upgrading to .NET 6 or .NET 7 is preferable. – Theodor Zoulias Dec 26 '22 at 16:41
  • 2
    Upgraded to .NET 6. I also installed Entity Framework 6.4.4 for other SQL connections. Parallel.ForEach was not working for all orders. Approximately 8 of 10 orders was completed at Parallel.ForEach. ForEachAsync works fine. Thanks @TheodorZoulias – Ali Karaçay Dec 26 '22 at 17:00
1

Since you're using asynchronous code, you should be using asynchronous concurrency, not (multithreaded) parallelism. Parallel.ForEachAsync will do both, but it's not available on older runtimes.

You can do just asynchronous concurrency by using Select and then Task.WhenAll:

var tasks = orders.Select(async order =>
{
  //Get Order Details
  //Generate getOrderDetailsContent with order 
  var getOrderDetailsResponse = await client.PostAsync(getOrderDetailsUrl, getOrderDetailsContent);
  var getOrderDetailsResponseString = await getOrderDetailsResponse.Content.ReadAsStringAsync();
  var getOrderDetails = JsonSerializer.Deserialize<getOrderDetailsResponse>(getOrderDetailsResponseString);

  //Create Order
  //Generate createOrderContent with GetOrderDetails
  var createOrderResponse = await client.PostAsync(createOrderUrl, createOrderContent);
  var createOrderResponseString = await createOrderResponse.Content.ReadAsStringAsync();
  var createOrder = JsonSerializer.Deserialize<createOrderResponse>(createOrderResponseString);

  //Create Log
  //Generate createLogContent with CreateOrderResponse
  var createLogResponse = await client.PostAsync(createLogUrl, createLogContent);
  var createLogResponseString = await createLogResponse.Content.ReadAsStringAsync();
  var createLog = JsonSerializer.Deserialize<createLogResponse>(createLogResponseString);

  return createLog;
}).ToList();
await Task.WhenAll(tasks);
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Stephen I just posted [a suggestion](https://github.com/dotnet/runtime/issues/82359 "Enable using the Parallel.ForEachAsync without a scheduler: invoke the body on the current thread") on GitHub about configuring the `Parallel.ForEachAsync` for pure asynchronous concurrency, without parallelism. Feel free to share your opinion about the suggestion, or if you have something else to add! – Theodor Zoulias Feb 19 '23 at 07:42