Search code examples
c#parallel-processingazure-functionsasp.net-core-webapiparallel.foreach

Run asynchronous methods in parallel, but ensure the return have data


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()
                        );

Solution

  • 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.