C# Asynchronous Programming Basics

C# Asynchronous Programming Basics

what is asynchronous

When starting a program, the system creates a new process in memory, and a process is a collection of resources that make up a running program. These resources include virtual address space, file handles, and many other things that a program needs to run.

Inside the process, the system creates a kernel object called a thread, which represents the actual executing program. Once the thread is established, the system will start the execution of the thread at the beginning of the first line of the Main method.

Regarding threads, we need to understand the following knowledge points:

  • By default, a process will only contain one thread, which is executed from the beginning to the end of the program. Generally, we call it the UI thread or the main thread.
  • A thread can spawn other threads, but note that all thread resources are requested by the program from the OS. So at any point in time, a process may contain multiple threads in different states executing different parts of the program.
  • If a process has multiple threads, they share the resources of the process.
  • A process is the smallest unit of program execution flow, and the unit that the system schedules for the processor is a thread rather than a process.

Why learn asynchronous programming

In many cases, the single-threaded model can lead to unacceptable behavior in terms of performance or user experience.

async/await

Let's look at an example first:

In the following example we use the asynchronous method to download strings from two websites.


using System;
using System.Diagnostics;
using System.Net;
using System.Threading.Tasks;

namespace MultiThreadTest_1
{
    
    
    internal class Program
    {
    
    
        public static void Main(string[] args)
        {
    
    
            MyDownloadString myDownloadString = new MyDownloadString();
            myDownloadString.DoRun();
        }
    }

    public class MyDownloadString
    {
    
    
        //使用Stopwatch记录时间
        Stopwatch _stopwatch = new Stopwatch();
        /// <summary>
        /// 执行主方法
        /// </summary>
        public void DoRun()
        {
    
    
            _stopwatch.Start();
            //执行两个异步方法分别获得两个网站的字符串长度
            Task<int> task = CountCharacterAsync(1, "https://www.bilibili.com/");
            Task<int> task2 = CountCharacterAsync(1, "https://www.processon.com/");
            //最后输出结果
            Console.WriteLine($"Chars in https://www.bilibili.com/ {
      
      task.Result}");
            Console.WriteLine($"Chars in https://www.processon.com/ {
      
      task2.Result}");
            _stopwatch.Stop();
        }
		//这里是异步方法
        public async Task<int> CountCharacterAsync(int id, string url)
        {
    
    
            //使用WebClient类来进行相应资源的下载
            WebClient webClient = new WebClient();
            Console.WriteLine($"Download Start At {
      
      _stopwatch.ElapsedMilliseconds}ms");
            //需要执行异步操作的表达式
            string str = await webClient.DownloadStringTaskAsync(new Uri(url));
            Console.WriteLine($"Download End At {
      
      _stopwatch.ElapsedMilliseconds}ms");
            return str.Length;
        }
    }
}

Results of the:

Download Start At 1ms
Download Start At 182ms
Download End At 456ms
Download End At 1123ms
Chars in https://www.bilibili.com/ 87599
Chars in https://www.processon.com/ 28559

Process finished with exit code 0.

It can be seen from this that after using the asynchronous method, the program is no longer executed sequentially.

Structure of the async/await feature

First of all, let's know two concepts: synchronous and asynchronous

If a method calls a method and waits for the method to perform all processing before proceeding, we call such a method a synchronous method, which is also the default form.

If a method returns to the calling method before completing all its work, we call such a method an asynchronous method. It can be realized by using the async/await feature provided by #.

This feature consists of three parts:

  • Invoke method: This method invokes the asynchronous method and then continues execution (possibly on the same thread or a different thread) while the asynchronous method performs its task.

  • Asynchronous method: The method marked by the async keyword is an asynchronous method, which performs its work asynchronously and then returns to the calling method immediately.

  • await expression: It is marked by await and is used inside the asynchronous method to indicate the operation that needs to be executed asynchronously. An asynchronous method can contain any number of await expressions, but the compiler will issue a warning if it contains none. Usually async and await appear together.

Take the example above as an example:

DoRun is the calling method, CountCharacterAsync is the asynchronous method, and webClient.DownloadStringTaskAsync is the await expression.

What is an asynchronous method

As mentioned above, an asynchronous method will return to the calling method immediately before completing its work, and then complete its work when the calling method continues to execute.

Syntactically asynchronous methods have the following characteristics:

  • The method header contains the async keyword.
  • Contains one or more await expressions.
  • Must have one of the following three return types:
    • void
    • Task
    • Task<generic type>
    • ValueTask<generic type>
  • Any type that has a publicly accessible GetAwaiter method.
  • The formal parameters of an asynchronous method can be of any type and any number, but cannot be out or ref parameters.
  • By convention, asynchronous methods should end with the suffix Async.
  • In addition to methods, both lambda expressions and anonymous methods can be asynchronous methods.

Let's introduce it in detail:

  • Asynchronous methods must contain the async keyword in the method header and must precede the return type. The type is just an identifier to indicate that the method is an asynchronous method, which contains one or more await expressions, and has no asynchronous operation itself.
    • The async keyword is a context keyword, which means that it can also be used as an identifier in addition to being a method modifier.
  • The return value type must be one of the following types.
    • Task: If the calling method does not need to return a value from the asynchronous method, but needs to check the status of the asynchronous method, the asynchronous method can return an object of type Task. In this case async methods cannot return anything if they contain any return statements. The Task type will be encapsulated for us by the compiler.
    • Task<generic type>: If our calling method needs to obtain a value of type T from the asynchronous method at this time, then the return value type of the asynchronous method needs to select this as the return. The calling method can get this value by reading the Result property in the Task type.
    • ValueTask<generic type>: This is a value-type Task object. In some cases, using ValueTask will be more efficient than using Task.
    • void: If the calling method just wants to blame the asynchronous method without any further interaction with him, we can use void return as the return value, in which case we call it call and forget.
    • Any type of GetAwaiter method that is accessible, we'll talk about this situation shortly.
  • You may find that in the previous example, the return value type is Task, but the method body does not contain any return statement that returns the Task type. Instead, at the end of the method, we return an int type of data. We note the conclusion first, and then elaborate on it later. Any asynchronous method returning type Task must return a value of type T or a type that can be implicitly converted to type T.

Asynchronous method control flow

The structure of an asynchronous method consists of three distinct areas, which are as follows:

  • The part before the first await expression:
    • All code from the beginning of the method up to the first await expression. This section should contain only a small amount of code that doesn't take long to process.
    • This area is executed synchronously until the await expression is reached.
  • await expression:
    • Represents a task that will be executed asynchronously.
  • Subsequent part:
    • The rest of the code in the method after the await expression. Contains its operating environment, such as the thread information, the variable value in the current scope, and other information required when the await expression needs to be re-executed after completion.

When the await expression is reached, the asynchronous method returns control to the calling method. If the return value type of the method is Task-related, the method will create a Task object to represent the current task that needs to be completed asynchronously and its follow-up, and then return the Task to the calling method. At this point, the calling method can continue to execute downwards, and the code in the asynchronous method will complete the following tasks:

  • Tasks that execute await expressions asynchronously
  • Execute the subsequent part when the await expression completes
  • If an await expression is encountered in the subsequent part, it will be handled in the same way.
  • If the subsequent part encounters a return statement, set the corresponding Task data, if present, and exit the control flow.

When the control flow of the above-mentioned asynchronous method is executed, since the await expression control is returned at the beginning, while the asynchronous method is executed, the control flow of the calling method is also continuing, and the Task-related return is obtained from the asynchronous method After the type, when the calling method needs the actual value, it refers to the Result property of the Task or ValueTask object. At that time, if the asynchronous method sets the property, the calling method can get the value and continue, otherwise it will pause and wait for the property to be set (threads are blocked asynchronously, and coroutines can be used in Unity to prevent threads from being blocked asynchronously) , and then continue execution.

One thing that many people may not understand is the type of object returned when the control flow of our asynchronous method encounters await for the first time. This return value type is the return type in the async method header. Has nothing to do with the return value of the await expression.

Another point that may be confusing is that when the return statement of an asynchronous method "returns" a result or reaches the end of the asynchronous method, he does not really return a value, he just exits the current asynchronous method, and the value will be set in In the Result property of the previously returned Task object.

await-expression

The await expression specifies a task to be executed asynchronously. Its syntax is shown below, consisting of the await keyword and a "task". The task may or may not be an object of type Task. By default, this task is executed asynchronously on the current thread.

await task

A task is an instance of the awaitable type. The awaitable type refers to the type that contains the GetAwaiter method, which has no parameters and returns an object of awaiter type.

C# provides us with a large number of awaitable type methods, which we can use directly. Their return values ​​are all of type Task or ValueTask.

Although there are many methods returning Task or ValueTask types in the current C# BCL, you may still need to write your own methods as tasks for await expressions. The easiest way is to use the Task.Run method to create a Task. A very important point about Task.Run is that it will run your method on a different thread.

Task.Run requires us to pass in a delegate. Commonly used Task.Run overloads are as follows:

return type method signature
Task Run(Action func)
Task Run(Action func, CancellationToken token)
Task Run(Func func)
Task Run(Func func, CancellationToken token)
Task Run(Func func)
Task Run(Func func, CancellationToken token)
Task Run(Func<Task> func)
Task Run(Func<Task> func, CancellationToken token)

cancel an asynchronous operation

Some .NET asynchronous methods allow you to request termination of execution. You can also add this feature to your own asynchronous methods soon. In the System.Threading.Tasks namespace there are two classes designed for this purpose: CancellationToken (struct) and CancellationTokenSource (class). You can understand the relationship between these two classes as the relationship between the lightweight version and the original version. The properties in CancellationToken are all referenced from CancellationTokenSource.

  • The CancellationToken object contains information about whether a task should be cancelled.
  • A task that owns a CancellationToken object needs to periodically check its token status. If the IsCancellationRequested property of the CancellationToken object is true, the task needs to stop its operation and return. The code that periodically checks the status of the token needs to be written by ourselves.
  • CancellationToken is irreversible and can only be used once, that is, once the IsCancellationRequested property is set to true, it cannot be changed. Simply put, CancellationTokenSource only provides the method Cancel() that is modified to true, but does not provide a method that is modified to false.

waiting task

There are two types of waiting tasks: synchronous waiting in the calling method, and asynchronous waiting in the asynchronous method

Waiting synchronously in the calling method, the calling method will not continue to execute until the task is completed, the APIs involved:

Task.Wait,Task.WaitAll,Task.WaitAny

Asynchronously wait in the asynchronous method, the calling method will not wait because the asynchronous method waits, on the contrary, the calling method will continue to execute. APIs involved:

Task.WhenAll,Task.WhenAny

When we use asynchronous await, our await expression becomes calling Task.WhenAll or Task.WhenAny.

Task.Delay method

The Task.Delay method creates a Task object that suspends its processing in the thread and completes after a certain amount of time. Unlike Thread.Sleep blocking threads, Task.Delay will not block threads, and threads can continue to process other work. Usually used in conjunction with await expressions.

When testing, you can use this method to simulate time-consuming asynchronous operations.

Task. Yield method

In computers, threads are a very precious resource. When awaiting an asynchronous task (function), it will first judge whether the Task has been completed. If it has been completed, it will continue to execute and will not return to the caller. The reason is to avoid thread switching as much as possible, because the code behind the await It is likely to be executed by another different thread, and Task.Yeild() can be forced to return to the caller, or actively give up the execution right to give other Tasks a chance to execute.

As shown in the following code:

public static async Task OP1()
{
    
    
     while (true)
     {
    
    
         await Task.Yield();//这里会捕捉同步上下文,由于是控制台程序,没有同步上下文,所以默认的线程池任务调度器变成同步上下文
                                     //也就是说后面的代码将会在线程池上执行,由于线程池工作线程数量设置为1,所以必须主动让出执行权,让其他的
                                     //任务有执行的机会
         Console.WriteLine("OP1在执行");
         Thread.Sleep(1000);//模拟一些需要占用CPU的操作
     }
}
public static async Task OP2()
{
    
    
     while (true)
     {
    
    
         await Task.Yield();
         Console.WriteLine("OP2在执行");
         Thread.Sleep(1000);
     }
}
static async Task Main(string[] args)
{
    
    
     ThreadPool.SetMinThreads(1, 1);
     ThreadPool.SetMaxThreads(1, 1);
     //Task.Run()方法默认使用线程池任务调度器执行任务,由于主线程不是线程池线程,所以使用Task.Run()
     var t = Task.Run(async () =>
     {
    
    
         var t1 = OP1();
         var t2 = OP2();
         await Task.WhenAll(t1, t2);
     });
     await t;
     Console.ReadLine();
}

Using asynchronous lambda expressions

Same as method used, here is an example of usage:

Action action = async () =>
{
    
    
    //await表达式必须是一个awaiter类型。
    await Task.Run(() =>
    {
    
    
        Console.WriteLine("123");
    });
};

BackgroundWorker

The async/await feature is more suitable for small unrelated tasks that need to be done in the background. But at some point, you may need to create another thread, run continuously in the background to complete a certain task, and need to communicate with the main thread from time to time. The BackgroundWorker class was born for this.

main members:

Attributes

attribute name attribute access rights Property introduction
WorkReportsProgress R/W Whether the current thread can report the status to the main thread, hope that the report is set to true
WorkerSupportsCanellation R/W Whether to support canceling the thread from the main thread, hope can be canceled set to true
IsBusy R
CancellationPending R Determine whether the thread should be stopped

method

method name method introduction
RunWorkerAsync Call this method to get the background thread and execute the DoWork callback function
CanelAsync This method will set the CancellationPending property to true. Like CanellationToken, this process is collaborative
ReportProgress This method is called when the background thread wants to report status to the main thread

event

event name event introduction trigger timing
DoWork The callback function attached to the DoWork event contains the code you want to execute on a background independent thread. The main thread executes the DoWork event by calling RunWorkerAsync Trigger DoWork when the background thread starts
ProgressChanged The background thread communicates with the main thread by calling the ReportProgress method, and this event will be triggered at that time, and the main thread can use the callback function attached to this event to process the event ProgressChanged is triggered when the background task reports progress
RunWorkerCompleted The callback function attached to this event should contain the code that needs to be executed after the background thread exits Trigger RunWorkerCompleted when the background worker thread exits

Other asynchronous programming patterns

BeginInvoke和EndInvoke

Both BeginInvoke and EndInvoke are delegated methods. When calling BeginInvoke, the actual parameters in the parameter list are composed as follows:

  • The parameters required by the reference method (the reference method is the method that will be executed on the new thread);
  • Two additional parameters - the callback parameter and the status parameter

BeginInvoke obtains a new thread in the thread pool and causes the referenced method to start running in the new thread.

BeginInvoke returns to the calling thread a reference to an object that implements the IAsyncResult interface. This interface reference contains the current state of the asynchronous method running in the thread pool. The original thread can then continue executing. The EndInvoke method is used to obtain the value returned by the asynchronous method call and release the resources used by the thread. EndInvoke has the following properties:

  • It takes a reference to the IAsyncResult object returned by BeginInvoke as a parameter, and finds the thread it is associated with.
  • If the thread pool thread has exited, EndInvoke does the following:
    • Cleans up the state of the exiting thread and releases its resources.
    • Find the value returned by the referenced method and return it as the return value.
  • If the thread pool thread is still running when EndInvoke is called, the calling thread stops and waits for it to complete. Then clean up the state of the exiting thread and release its resources, find the value returned by the reference method and return it as the return value. Because EndInvoke cleans up the opened thread, you must ensure that EndInvoke is called for each BeginInvoke.
  • If the asynchronous method triggers an exception, it will be thrown when EndInvoke is called.
  • EndInvoke provides all the output of an asynchronous method call, including ref and out parameters. If the delegate's reference method has ref or out parameters, they must be included in the EndInvoke parameter list before the IAsyncResult object reference.

wait until complete

In this mode, after initiating the asynchronous method and doing some other processing, the original thread is interrupted and waits for the asynchronous method to complete before continuing, and a synchronous wait occurs.

polling

In polling mode, the original thread initiates an asynchronous method call, does some other processing, and then uses the IsCompleted property of the IAsyncResult object to periodically check whether the opened thread is complete. If the asynchronous method has completed, the original thread calls EndInvoke and continues, otherwise it does some other processing and polls for checks.

call back

In the previous wait-until-completion and polling modes, the originating thread continues its flow of control only after knowing that the opening thread has completed. Then he gets the result and continues.

The difference in the callback mode is that once the initial thread initiates the asynchronous method, it takes care of itself and does not consider synchronization. When a method call ends, the system calls a user-defined method to process the result and calls the delegate's EndInvoke method. This user-defined method is called a callback method or callback. Here you need to use two additional parameters of BeginInvoke.

BeginInvoke uses two additional parameters:

  • The first parameter callback is the name of the callback function, and the callback function must be of the AsyncCallBack delegate type.
  • The second parameter state can be null, and this parameter can be obtained through the AsyncState property of IAsyncResult.

Call EndInovke in the callback method. If you want to call EndInvoke, you need to know the reference of the delegate object, and he is in the initial thread. At this time, if the state does not have a special purpose, we can pass in the delegate object through the state, and in the callback This reference is obtained through the AsyncState of IAsyncResult in the method.

Guess you like

Origin blog.csdn.net/BraveRunTo/article/details/120611366