2

Can we call multiple asynchronous methods and ensure that the returned object must wait till all methods run are complete?

Here is the scenario. I have a main method which calls other several methods. Each sub method calls a separate ExecuteAsync API to return results. Some of the methods are dependent on the result returned from previous methods. Running them sequentially this way, it takes much time to complete a request. Can we make parallel call to each method and the final returned object should have all of the data?

Below is a sample code for what I am trying to achieve.

public Student GetStudentDetails()
{
    Student objStudent = new Student();

    objStudent.Name = Helpers.GetStudentName(); // Takes 1 second

    objStudent.CoursesIDs = Helpers.GetStudentCourses(); //Returns a list of string > Takes 1 second
    foreach (String courseID in objStudent.CoursesIDs)
    {
        string courseName = Helpers.GetCourseName(courseID); // Each Call takes 1 second. 10 courses X 10 seconds
        objStudent.CourseName.Add(new Courses { ID:courseID, Title:courseName });
    }

    objStudent.MarksIDs = Helpers.GetStudentMarks(); //Returns a list of string > Takes 1 second
    foreach (String MarksID in objStudent.MarksIDs)
    {
        string ActualMarks = Helpers.GetActualMarks(MarksID); // Each Call takes 1 second. 10 calls X 10 seconds
        objStudent.Marks.Add(new Marks { ID:MarksID, Title:ActualMarks });
    }

    return objStudent;
}

This is just sample code to get overall idea. I had to implement more bigger calls than this but I believe the idea should be same.

How I can make my function to run GetStudentName, GetCourseName and GetActualMarks simultaneously and the objStudent should have all the data?

I tried running the methods sequentially, this way it work fine but takes 30 to 40 seconds to return all data for a student.

I also tried running them parallel by splitting it into multiple tasks using below but most of the returned values are just null.

 Task.Run(() => mySubMethods );

P.S: I am using RestSharp and in each of my method which returns me student related data I am using ExecuteAsync. For example.

var client = new RestClient(APIURL);
RestRequest request = new RestRequest();
RestResponse response = client.ExecuteAsync(request).Result;

I appreciate any helpful approach.

Update 1: I am trying to call my async Task methods using parllel invoke, however this do not wait for all of my methods to finish the work.

If any of methods takes more time to complete, it returns nothing. For example, I have three methods set as async, method 3 takes 10 seconds while method 1&2 takes 5 seconds to complete. After 5 seconds, it shows the result of first two methods while third one returns null. I want system to wait for the last method to complete its work. I am trying something similar.

      Parallel.Invoke(
                        async () => val1 = await Task.Run(() =>
                          {
                              return GetVal1();
                          }),
                        async () => val2 = await Task.Run(() =>
                          {
                                return GetVal2();
                           }),
                        async () => val3 = await Task.Run(() =>
                          {
                              return GetVal3();
                          }),
                 );

I have also tried below approach but same results. It leaves behind if any methods takes more time to complete.

  Parallel.Invoke(
                          async () => val1 = await GetVal1(),

                          async () => val2 = await GetVal2(),

                          async () => val3 = await GetVal3()
                        );
  • 1
    There are no async calls in your code. Are you asking how to run the requests in parallel? – PMF Jan 08 '23 at 12:01
  • Have you seen this article - https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/ - which describes "making breakfast" with async tasks? You don't show any async calls, but I'm assuming your `Helper` methods are really async? – MrC aka Shaun Curtis Jan 08 '23 at 12:03
  • I am using RestSharp and in each of my method which returns me student related data i am using ExecuteAsync. For example. var client = new RestClient(APIURL); RestRequest request = new RestRequest(); RestResponse response = client.ExecuteAsync(request).Result; – Muhammad Zeeshan Tahir Jan 08 '23 at 12:10
  • 2
    Don't use `client.ExecuteAsync(request).Result` - instead, change it to `await client.ExecuteAsync(request)` and make your methods async. – Klaus Gütter Jan 08 '23 at 12:16
  • 1
    Related: [Parallel.ForEach and async-await](https://stackoverflow.com/questions/23137393/parallel-foreach-and-async-await) and also [Parallel.Invoke does not wait for async methods to complete](https://stackoverflow.com/questions/24306620/parallel-invoke-does-not-wait-for-async-methods-to-complete). The only `Parallel` method that supports async delegates is the `Parallel.ForEachAsync`. Btw please avoid stuffing a question with secondary questions, because it can cause the question to be closed as "Needs more focus". Ask a new question instead. – Theodor Zoulias Jan 09 '23 at 17:54
  • 1
    Thanks. I just wanted to summarize the scenario at one place. I will create another question if required. Thank you for your support and solution. – Muhammad Zeeshan Tahir Jan 10 '23 at 08:24

2 Answers2

1

Actually, async calls won't help you much here. These will help you avoid that the parent process (e.g. the UI) doesn't freeze, but won't improve the speed of the whole call. You could split request and await, but that would increase the complexity of the code. I think the easiest thing here would be to use the Parallel class instead.

Something along the lines of:

object updateLock = new object();
objStudent.CoursesIDs = Helpers.GetStudentCourses();
Parallel.Foreach(objStudent.CoursesIDs, courseID => 
    {
        string courseName = Helpers.GetCourseName(courseID); 
        lock (updateLock)
        {
            objStudent.CourseName.Add(new Courses { ID:courseID, Title:courseName  } );
        }
   });

This queries all the courses in parallel. Overall this should be faster (assuming the server is capable of answering all requests simultaneously). Note the required lock, because the CourseName array now needs to be thread safe. This shouldn't have a performance impact since adding an entry to a list is several orders of magnitude faster than the query. Note that the CourseName list will be in random order now.

PMF
  • 14,535
  • 3
  • 23
  • 49
  • Thank you for your answer. This Parallel.Foreach is fine however i tried using Parallel.ForEachAsync which also seems to be solving the issue to some extent. However, when i use async in my Parallel.Invoke(); where i have to call multiple APIs at once, I do not get any results.. like Parallel.Invoke( async () => await Method1(),async () => await Method2()); it do not get me results. I get null from this method. – Muhammad Zeeshan Tahir Jan 09 '23 at 14:15
  • For that to work correctly, you need to set up the Methods to return Task, then you can await them and assign the result. See the other answer for this. – PMF Jan 09 '23 at 15:08
  • Yeah my methods already set as async Task, If any of methods takes more time to complete, it returns nothing. For example, I have three methods set as async, method 3 takes 10 seconds while method 1&2 takes 5 seconds to complete. After 5 seconds, it shows the result of first two methods while third one returns null. I want system to wait for the last method to complete its work. – Muhammad Zeeshan Tahir Jan 09 '23 at 15:40
  • I am trying something like this. Parallel.Invoke( async () => val1 = await Task.Run(() => { return GetVal1(); }), async () => val2 = await Task.Run(() => { return GetVal2(); }), async () => val3 = await Task.Run(() => { return GetVal3(); }), ); – Muhammad Zeeshan Tahir Jan 09 '23 at 15:42
  • Please edit your question and add these additional details. Code in comments is very hard to follow. – PMF Jan 09 '23 at 15:44
  • Done. Kindly notice the Update 1 in the question. – Muhammad Zeeshan Tahir Jan 09 '23 at 15:53
  • Thanks for the update. Maybe somebody can help, but according to me this should work. – PMF Jan 09 '23 at 18:11
1

First step: get rid of all .Result calls. Everywhere that you have a .Result replace it with await, add the async keyword in the method definition, and change the return type form X to Task<X>. You can also (optionally) append the Async suffix to the name of the method, to signify that the method is asynchronous. For example:

async Task<string> GetCourseNameAsync(int id)
{
    //...
    var client = new RestClient(url);
    RestRequest request = new RestRequest();
    RestResponse response = await client.ExecuteAsync(request);
    //...
    return courseName;
}

Second step: parallelize the GetCourseNameAsync method for each student by using the Parallel.ForEachAsync method (available from .NET 6 and later), configured with an appropriate MaxDegreeOfParallelism:

ParallelOptions options = new() { MaxDegreeOfParallelism = 4 };

Parallel.ForEachAsync(objStudent.CoursesIDs, options, async (courseID, ct) =>
{
    string courseName = await Helpers.GetCourseNameAsync(courseID);
    lock (objStudent.CourseName)
    {
        objStudent.CourseName.Add(new Courses { ID:courseID, Title:courseName });
    }
}).Wait();

The courseNames will be added in the objStudent.CourseName collection is the order of completion of the asynchronous operations, not in the order of the ids in originating list objStudent.CoursesIDs. In case this is a problem, you can find solutions at the bottom of this answer.

In the above example the Parallel.ForEachAsync is Waited synchronously, because the container method GetStudentDetails is not asynchronous. So it violates the async-all-the-way principle. This might not be a problem, but it is something that you should have in mind if you care about improving your application, and making it as efficient as possible.

The MaxDegreeOfParallelism = 4 is a random configuration. It's up to you to find the optimal value for this setting, by experimenting with your API.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Thanks Theodor for answer with such explanation. That actually solved like 70% of my problem where I had issues with looping the requests. However, I am still have some issues while invoking multiple methods at once with async. I have updated my question, kindly check the Update 1 for the problem description. – Muhammad Zeeshan Tahir Jan 09 '23 at 15:58