SoFunction
Updated on 2025-03-08

Detailed explanation of the definition and execution principle of asynchronous Action under MVC

The Controller Creation Wizard provided by Visual Studio creates a Controller type inherited from the abstract class Controller. Such a Controller can only define synchronization Action methods. If we need to define an asynchronous Action method, we must inherit the abstract class AsyncController. This article asks you about the definition methods and underlying execution principles of two different asynchronous Actions.

1. Request processing based on thread pool

Concurrent HTTP requests are handled through thread pool mechanisms. A web application maintains a thread pool inside. When an arriving request for the application is detected, an idle thread will be obtained from the pool to process the request. When the processing is completed, the thread will not be recycled, but will be re-released into the pool. The thread pool has the maximum capacity of a thread. If the created thread reaches this upper limit and all threads are in a "busy" state, the new HTTP request will be placed in a request queue to wait for a thread that has completed the request processing task to be re-released into the pool.

We call these threads used to handle HTTP requests Worker Threads, and this county is naturally called a worker thread pool. This thread pool-based request processing mechanism has the following two advantages:

  • Reuse of worker threads: Although the cost of creating threads is not as good as process activation, it is not a "single-time" thing. Frequently creating and releasing threads will cause great damage to performance. The thread pooling mechanism avoids always creating new worker threads to handle every request, and the created worker threads are greatly reused and ultimately improves the server's throughput capabilities.
  • Limitation of the number of worker threads: The limitation of resources has an upper limit to the server's ability to process requests, or the number of request concurrency that a certain server can process has a critical point. Once this critical point is exceeded, the entire service will crash because it cannot provide sufficient resources. Due to the thread pooling mechanism with good control over the number of worker threads, the number of requests processed by MVC cannot exceed the maximum allowable capacity of the thread pool, thus avoiding unlimited creation of worker threads in high concurrency situations, which will most likely cause the entire server to crash.

If the request processing operation is short, the worker thread can be promptly released into the thread pool for processing the next request after processing. However, for relatively time-consuming operations, it means that the worker thread will be exclusive to a request for a long time. If such operations are accessed frequently, in the case of high concurrency, it means that the thread pool may not be found to use to process the latest arrival request in time.

If we use an asynchronous method to handle such time-consuming requests, the worker thread can let the background thread take over, and it can be released into the thread pool in time for processing subsequent requests, thereby improving the throughput capability of the entire server. It is worth mentioning that asynchronous operations are mainly used for I/O binding operations (such as database access and remote service calls, etc.), rather than CPU binding operations, because the improvement of overall performance by asynchronous operations comes from: when the I/O device is processing a certain task, the CPU can release it to process another task. If time-consuming operations mainly rely on the operation of the native CPU, the asynchronous method will affect the overall performance due to thread scheduling and thread context switching.

2. Definition of two asynchronous Action methods

After understanding the necessity of defining an asynchronous Action method in AsyncController, let’s briefly introduce the definition of an asynchronous Action method. In general, asynchronous Action methods have two definition methods, one is to define them as two matching methods XxxAsync/XxxCompleted, and the other is to define a method with the return type Task.

XxxAsync/XxxCompleted

If we use two matching methods XxxAsync/XxxCompleted to define an asynchronous Action, we can implement the asynchronous operation in the XxxAsync method and present the final content in the XxxCompleted method. XxxCompleted can be regarded as a callback for XxxAsync. When the operations defined in the XxxAsync method are executed in an asynchronous manner, the XxxCompleted method will be automatically called. The definition method of XxxCompleted is similar to that of ordinary synchronization Action methods.

As a demonstration, I defined an asynchronous operation named Article in the following HomeController to render the article content with the specified name. We define the asynchronous reading of the specified article content in the ArticleAsync method, and the content read in the ArticleCompleted method is presented in the form of ContentResult.

public class HomeController : AsyncController
   {
     public void ArticleAsync(string name)
     {
       ();
       (() =>
         {
           string path = ((@"\articles\{0}.html", name));
           using (StreamReader reader = new StreamReader(path))
          {
            ["content"] = ();
          }
          ();
        });
    }
    public ActionResult ArticleCompleted(string content)
    {
      return Content(content);
    }
  } 

For asynchronous Action methods defined in the form of XxxAsync/XxxCompleted, MVC does not call the XxxAsync method in an asynchronous way, so we need to customize the execution of asynchronous operations in this method. In the ArticleAsync method defined above, we implement asynchronous reading of article content through parallel programming based on Task. When we define the asynchronous Action method in the form of XxxAsync/XxxCompleted, the Controller's AsyncManager property will be frequently used, which returns an object of type AsyncManager, which we will describe separately in the following section.

In the example provided above, we call the Increment and Decrement methods of the OutstandingOperations property of the AsyncManager at the beginning and end of the asynchronous operation to initiate notifications for MVC. In addition, we also use the dictionary represented by the Parameters property of AsyncManager to save the parameters passed to the ArticleCompleted method. The Key (content) of the parameters in the dictionary matches the parameter name of ArticleCompleted, so when the method ArticleCompleted is called, the parameter value specified through the Parameters property of AsyncManager will be automatically used as the corresponding parameter value.

Task returns value

If we adopt the asynchronous Action definition method above, it means that we have to define two methods for an Action. In fact, we can use one method to define the asynchronous Action, that is, let the Action method return a Task object representing the asynchronous operation. The asynchronous Action defined above in the form XxxAsync/XxxCompleted can adopt the following definition method.

 public class HomeController AsyncController
   {
     public Task<ActionResult> Article(string name)
     {
       return (() =>
         {
           string path = ((@"\articles\{0}.html", name));
           using (StreamReader reader = new StreamReader(path))
           {
            ["content"] = ();
          }
        }).ContinueWith<ActionResult>(task =>
          {
            string content = (string)["content"];
            return Content(content);
          });
    }
  }

The return type of the asynchronous Action method Article defined above is Task<ActionResult>, and we reflect the reading of the asynchronous file contents in the returned Task object. The callback operation presented to the file content is registered by calling the ContinueWith<ActionResult> method of the Task object, which will be automatically called after the asynchronous operation is completed.

As shown in the above code snippet, we still use the Parameters property of AsyncManager to implement the transfer of parameters between asynchronous operations and callback operations. In fact, we can also use the Result property of the Task object to achieve the same function, and the definition of the Article method is also rewritten into the following form.

 public class HomeController AsyncController
   {
     public Task<ActionResult> Article(string name)
     {
       return (() =>
         {
           string path = ((@"\articles\{0}.html", name));
           using (StreamReader reader = new StreamReader(path))
           {
            return ();
          }
        }).ContinueWith<ActionResult>(task =>
          {          
            return Content((string));
          });
    }
  }

3. AsyncManager

In the definition of asynchronous Action demonstrated above, we implement two basic functions through AsyncManager, namely passing parameters between asynchronous operations and callback operations and sending notifications of the start and end of the asynchronous operations to the MVC. Since AsyncManager plays an important role in asynchronous Action scenarios, it is necessary to introduce it separately. The following is the definition of AsyncManager.

 public class AsyncManager
   {  
     public AsyncManager();
     public AsyncManager(SynchronizationContext syncContext);
   
     public EventHandler Finished;
   
     public virtual void Finish();
     public virtual void Sync(Action action);
    
    public OperationCounter OutstandingOperations { get; }
    public IDictionary<string, object> Parameters { get; }
    public int Timeout { get; set; }
  }
   
  public sealed class OperationCounter
  {
    public event EventHandler Completed;  
    
    public int Increment();
    public int Increment(int value);
    public int Decrement();
    public int Decrement(int value);
    
    public int Count { get; }
  }

As shown in the code snippet above, AsyncManager has two constructor overloads, and non-default constructors accept a SynchronizationContext object representing the synchronization context as a parameter. If the specified synchronization context object is Null and the current synchronization context (represented by the static property Current of the SynchronizationContext) exists, the context is used; otherwise a new synchronization context is created. The synchronization context is used for the execution of the Sync method, that is, the Action delegate specified in the method will be executed in a synchronous manner in the synchronization context.

The core of AsyncManager is the ongoing asynchronous operation counter represented by the property OutstandingOperations, which is an object of type OperationCounter. The operation count is represented by the read-only attribute Count. When we start and complete asynchronous operations, we call the Increment and Decrement methods respectively to increase and introduce the counting operations. Increment and Decrement each have two overloads, as integer parameter value (the parameter value can be a negative number) represents an increase or decrease value. If the parameterless method is called, the increase or decrease value is 1. If we need to perform multiple asynchronous operations at the same time, we can operate the counter by following method.

 (3);
   
   (() =&gt;
   {
     //Async operation 1     ();
   });
   (() =&gt;
   {
    //Async operation 2    ();
  });
  (() =&gt;
  {
    //Async operation 3    ();
  });

For each change in count value caused by the Increment and Decrement method calls, the OperationCounter object will check whether the current count value is zero. If it means that all operations have been run, and if the Completed event is pre-registered, the event will be triggered. It is worth mentioning that the flag that indicates that all operations are completed is that the value of the counter is equal to zero, not less than zero. If we call the Increment and Decrement methods, the registered Completed event will not be triggered.

When AsyncManager is initialized, it registers the Completed event of the OperationCounter object represented by the property OutstandingOperations, so that the event is triggered to call its own Finish method. The default implementation of virtual method Finish in AsyncManager will trigger its own Finished event.

As shown in the code snippet below, the Controller class implements the IAsyncManagerContainer interface, which defines a read-only property AsyncManager to provide an AsyncManager object that assists in executing asynchronous Action. The AsyncManager object we use when defining the asynchronous Action method is the AsyncManager property integrated from the abstract class Controller.

  public abstract class Controller ControllerBase, IAsyncManagerContainer,...
   {
     public AsyncManager AsyncManager { get; }
   }
   
   public interface IAsyncManagerContainer
   {  
     AsyncManager AsyncManager { get; }
   }

4. Execution of Completed method

For asynchronous Action defined in the form of XxxAsync/XxxCompleted, we say that the callback operation XxxCompleted will be automatically called after the asynchronous operation defined in the XxxAsync method is completed. So how exactly is the XxxCompleted method executed?

The execution of an asynchronous Action is ultimately done by describing the BeginExecute/EndExecute method of the AsyncActionDescriptor object of the Action. Through the previous introduction of "Model binding", we know that the asynchronous Action defined by the XxxAsync/XxxCompleted form is represented by a ReflectedAsyncActionDescriptor object. When the ReflectedAsyncActionDescriptor executes the BeginExecute method, it registers the Finished event of the AsyncManager of the Controller object, so that the Completed method is executed when the event is triggered.

That is to say, the triggering of the Finished event for the AsyncManager of the current Controller marks the end of the asynchronous operation, and the matching Completed method will be executed at this time. Since the Finish method of AsyncManager will proactively trigger the event, we can make the Completed method execute immediately by calling the method. Since the Finish method is called when the Completed event of the OperationCounter object of AsyncManager is fired, the Completed method will also be automatically executed when the value of the calculator indicating that the currently executing asynchronous operation is zero.

If we execute three asynchronous operations simultaneously in the XxxAsync method in the following way, and call the AsyncManager's Finish method after each operation is completed, it means that the first asynchronous operation will result in the execution of the XxxCompleted method. In other words, when the XxxCompleted method is executed, there may be two asynchronous operations being executed.

  (3);
   
   (() =&gt;
   {
     //Async operation 1     ();
   });
   (() =&gt;
   {
    //Async operation 2    ();
  });
  (() =&gt;
  {
    //Async operation 3    ();
  });

If the execution of the XxxCompleted method is completely controlled through the asynchronous operation counting mechanism for completion, since the count detection and the triggering of the Completed event only occur when the Increment/Decrement method of the OperationCounter is executed, if we do not call these two methods when starting and ending the asynchronous operation, will XxxCompleted be executed? Similarly, taking the asynchronous Action for reading/displaying article content in the previously defined terms as an example, we will define the increment and Decrement methods of the OutstandingOperations property of the AsyncManager in the ArticleAsync method as follows. Can the ArticleCompleted method still run normally?

  public class HomeController AsyncController
   {
     public void ArticleAsync(string name)
     {
       //();
       (() =>
         {
           string path = ((@"\articles\{0}.html", name));
           using (StreamReader reader = new StreamReader(path))
          {
            ["content"] = ();
          }
          //();
        });
    }
    public ActionResult ArticleCompleted(string content)
    {
      return Content(content);
    }
  }

In fact, ArticleCompleted will still be executed, but this way we cannot ensure that the article content is read normally, because the ArticleCompleted method will be executed immediately after the ArticleAsync method is executed. If reading the article content is a relatively time-consuming operation, the content parameter of the ArticleCompleted method of the article content has not been initialized yet during execution. How is ArticleCompleted executed in this case?

Reason and simplicity, the BeginExecute method of ReflectedAsyncActionDescriptor will call the Increment and Decrement methods of the OutstandingOperations property of AsyncManager respectively before and after executing the XxxAsync method. For the example we gave, the Increment method is called to make the calculator value become 1 before the ArticleAsync is executed, and the ArticleAsync is then executed, and since the method reads the specified file contents in an asynchronous manner, it will be returned immediately. Finally, the Decrement method is executed to make the counter value 0, and the Completed event of the AsyncManager is triggered and causes the execution of the ArticleCompleted method. At this time, the reading of the file content is in progress, indicating that the content parameters of the article content have naturally not been initialized.

The execution mechanism like ReflectedAsyncActionDescriptor also requires us to use AsyncManager, that is, the increase operation of the unfinished one-step operation counter should not occur in the asynchronous thread. The definition of the Increment method for the OutstandingOperations property of AsyncManager as shown below is incorrect.

  public class HomeController AsyncController
   {
     public void XxxAsync(string name)
     {
       (() =&gt;
         {
           ();
            //...
            ();
        });
    }
    //Other members  } 

The following is the correct definition method:

 public class HomeController AsyncController
   {
     public void XxxAsync(string name)
    {
      ();
       (() =&gt;
         {
           //...
           ();
        });
    }
    //Other members  } 

Finally, I would like to emphasize that whether it is explicitly calling the Finish method of AsyncManager or the Increment method of the OutstandingOperations property of AsyncManager, the value of the counter becomes zero, just allowing the XxxCompleted method to be executed cannot really prevent the execution of asynchronous operations.

5. Timeout control of asynchronous operation

Although asynchronous operations are suitable for relatively time-consuming I/O-bound operations, it does not mean that there is no limit on the time for execution of a one-step operation. The asynchronous timeout timeout is represented by the AsyncManager's integer property Timeout, which represents the total number of milliseconds of the timeout timeout, and its default value is 45000 (45 seconds). If the Timeout property is set to -1, it means that the asynchronous operation execution no longer has any time limit. For asynchronous Action defined in the form of XxxAsync/XxxCompleted, if XxxCompleted is not executed in the specified timeout limit after XxxAsync is executed, a TimeoutException will be thrown.

If we define an asynchronous Action in the form of a return type Task, the execution time of the asynchronous operation reflected by the Task is not limited by the Timeout property of the AsyncManager. We define an asynchronous Action method called Data in an asynchronous way to obtain the data as a Model and render it through the default View. However, there is an infinite loop in the asynchronous operation. When we access the Data method, the asynchronous operation will be executed unlimitedly, and there will be no TimeoutException exception.

  public class HomeController AsyncController
   {
     public Task&lt;ActionResult&gt; Data()
     {
       return (() =&gt;
       {
         while (true)
         { }
         return GetModel();
          
      }).ContinueWith&lt;ActionResult&gt;(task =&gt;
      {
        object model = ;
        return View();
      });
    }
    //Other members  }

There are two special features in the MVC application programming interface for customizing the timeout time limit for asynchronous operation execution. They are AsyncTimeoutAttribute and NoAsyncTimeoutAttribute defined below, both defined in the namespace.

  [AttributeUsage( | , Inherited=true, AllowMultiple=false)]
   public class AsyncTimeoutAttribute ActionFilterAttribute
   {
     
     public AsyncTimeoutAttribute(int duration);
     public override void OnActionExecuting(ActionExecutingContext filterContext);  
     public int Duration { get; }
   }
   
  [AttributeUsage( | , Inherited=true, AllowMultiple=false)]
  public sealed class NoAsyncTimeoutAttribute AsyncTimeoutAttribute
  {
    // Methods
    public NoAsyncTimeoutAttribute() base(-1)
    {
    }
  }

From the definition given above, we can see that both of these characteristics are ActionFilters. The constructor of AsyncTimeoutAttribute accepts an integer representing the timeout (in milliseconds) as its parameter. It sets the specified timeout to the Timeout property of the AsyncManager of the current controller by overriding the OnActionExecuting method. NoAsyncTimeoutAttribute is the successor to AsyncTimeoutAttribute, which sets the timeout time limit to -1, meaning it lifts the limit on timeout.

From the AttributeUsageAttribute definition applied to these two features, they can be applied to both classes and methods, meaning we can apply them to Controller types or asynchronous Action methods (only valid for XxxAsync methods, not to XxxCompleted methods). If we apply them to both the Controller class and the Action method, the feature for the method level will undoubtedly have higher priority.

The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.