The essence of C# asynchronous multithreading, context flow and synchronization

introduction

.NET colleagues have really been talking about async and await for a long time. During this period of time, I saw discussions on this aspect, and finally did not draw any conclusions. In fact, it is not that complicated to understand this thing, it is simple In essence, in one sentence, the asynchronous essence of async and await is the flow of state machine + thread environment context, the state machine advances the execution, and the context switches the environment.

When the state machine advances forward, the first movenext will save the environment context of the current thread, and then the TaskScheduler will schedule whether to go to the thread pool to get a new thread to execute the task, and when the subsequent advance to the last movenext, set the Good result, after the exception, the callback needs to run in the environment context before calling await, which is the environment context, not the thread.

So the current environment context is the context of thread A before await, and it may be the environment context of thread B after the end of await, and asynchronous is asynchronous, thread is thread, asynchronous is not necessarily multi-threaded, these two are not equivalent, For the source code analysis of async and await, you can read the blog written before https://www.cnblogs.com/1996-Chinese-Chen/p/15594498.html. This article talks about a part of the source code, which may not be very clear , I only talked about a sequence of async await execution without too much description of the environment context. Next, I will talk about some environment contexts, knowledge of synchronization contexts, and the encapsulation of synchronization contexts by frameworks in cs programs.

Environment context ExecutionContext

ExecutionContext represents the execution context that manages the current thread. For this class, the official website explains that the ExecutionContext class provides a single container for all information related to logical execution threads. In the .NET Framework, this includes the security context, calling context, and synchronization context.

In .NET Core, security context and invocation context are not supported, however, impersonation context and culture usually flow through execution context.

To put it simply, this class is a container that stores all the environmental information of the current thread. It is slightly different in net framework and net core. The latter does not include synchronization context. Regarding synchronization context and ExecutionContext, you can see another comparison on the official website Good article https://devblogs.microsoft.com/pfxteam/executioncontext-vs-synchronizationcontext/ This article explains async await and context in more detail.

So at the beginning we said that one of the essence of asynchrony is context flow, so what is flow, how to flow, this class represents the container that stores the information of the current thread, then we copy this container, and then put it in another thread Go, then another thread can get all the information inside our previous thread. The simple understanding is that when I moved, I packed all my things and put them in my new house, so this new house also has my before moving. The information, this is the context flow, next, let’s look at the actual example in the code

public class TestTask
{
    public static AsyncLocal<int> id;
}
private async void button1_Click(object sender, EventArgs e)
{
    //var sss = new MyTask(() => { Console. WriteLine(111); });
    //await sss;
    var exce = ExecutionContext. IsFlowSuppressed();
    TestTask.id = new AsyncLocal<int>() { Value = 1 };
    // var asynclo=ExecutionContext. SuppressFlow();
    var con1 = ExecutionContext. Capture();
    var a = ExecutionContext. SuppressFlow();
    exce = ExecutionContext. IsFlowSuppressed();
    await Task. Delay(1000);
    var con2 = ExecutionContext. Capture();
    ExecutionContext.Run(con2, s =>
    {
        var sss = TestTask.id.Value;
    }, null);
    await Task. Delay(1000);
    ExecutionContext. Restore(con1);
    var sssa = TestTask.id.Value;
}

In the above code, I first defined an AsyncLocal to store a variable of int type, added a button to the winform interface, wrote the following code in the click event, and called the ExecutionContext.IsFlowSuppressed method in the first line of code , this method is to judge whether to stop the flow of the current context. At the beginning of the operation, the return result is False, indicating that we have not stopped the flow and can flow normally. In the second line of code, we assign a value to the AsyncLocal variable and set Value is 1;

In the third line, we use the ExecutionContext.Capture method, which is to capture the current context information, and then assign it to the con1 variable. Going down, we call the SuppressFlow method, which prevents the flow of the current context. That is to say, this context is different from the context after await, and then we return true when judging IsFlowSuppressed, stopping the flow, and then we asynchronously delay for 1 second, and then we capture the context information of the current thread after asynchronous , and then here we capture the context information of our thread, and then call the ExecutionContext.Run method. This method is to run the delegate code of the second parameter in the specified context. Do we use this run method? In fact, it does not affect the demonstration effect. In this code, the id.Value we get is different from the above one. The default value 0 is obtained instead of the 1 defined above. This is because we stopped the context flow, causing the await before and after It’s not the same context, so we can’t get this Value. If we don’t call SuppressFlow, then after await, it will be the previous context information, and the obtained Value is the original 1. Going down, let’s delay, and then Call the Restore method. This method replaces the context of the current thread with the specified context information, restores the specified context information to the current thread, and then obtains a Value of 1.

There are several methods in the ExectuionContext method. Capture is to statically capture the current context information. CreateCopy is an instance method that returns a copy of the current context information. IsFlowSuppressFlow judges whether to stop context flow. SuppressFlow stops context flow. Restore captures The context information is restored to the current thread. Of course, there is another method, which corresponds to the SuppressFlow method. One is to stop and the other is to restore. It is called RestoreFlow to restore the flow of the current context between asynchronous threads, but it is not suitable for this in the async scenario. The situation is that there is an error report. This error is that the current context has not stopped the context flow. Why is this? Let me explain.

We all know that the development of threads is Thread, Threadpool, and now Task, and then Task is encapsulated based on Threadpool, then the thread after we use await Task is specified by Threadpool, then the thread specified by him is not necessarily The thread before await causes you to be prompted that the context flow has not stopped when you restore the context flow after await, because this problem is caused by different threads, that is to say, your SuppressFlow is another thread, and the one after await is another thread. If you RestoreFlow another thread, it will definitely report an error, so we need to use the Restore method to restore the context information we captured before to the current thread, so that we can get the result when we get the Value later.

One more problem that needs to be explained in this section is that in the previous paragraph, we said that the threads of Task are all allocated by Threadpool, which will cause the threads executed by certain codes to be allocated by Threadpool, and this problem will lead to the original The Thread aspect cannot be used for thread data transfer. For example, ThreadLocal and ThreadStaticAttribute features, these are all unplayable, because we don’t know whether the threads of Task and Threadpool are the same before and after. Then each thread of ThreadLocal and ThreadStatic It is the data of each thread that we will not be able to obtain. This is something that everyone needs to understand when using it.

SynchronizationContext

The ExecutionContext mentioned above can be called the thread environment context, and the SynchronizationContext provides the basic function of propagating the synchronization context in various synchronization models. It can be called thread synchronization context. If ExecutionContext is the container of the entire environment information, then this class is the interface that exposes the entire environment information to you. Although Execution can also synchronize between different threads, it is not good for you to expose everything. You can Let him know everything about your house, obviously not, each thread of this SynchronizationContext can set its own synchronization context information, you can rewrite this class, or you can use this class to dispatch information asynchronously or synchronously In the context of a certain thread, use the Send method synchronously, and pass in the SendOrPostCallBack delegate and the parameters required by the delegate.

If we get the SynchronizationContext.Current in the thread is empty, null, we can create a SynchronizationContext variable, var context=new SynchronizationContext(); and then call SynchronizationContext.SetSynchronizationContext(context); to set the synchronization context for the current thread, you need to When other threads are synchronized, only the context.Post method or the context.Send method is required to synchronize.

In addition, in the CS program, winform and wpf are rewritten for the SynchronizationContext class to meet the needs of the framework level, because in the CS program, all operations such as creation, modification and deletion of controls should be completed by the UI thread, if An error will be reported across threads. At the same time, async and await are used in the cs program. The environment context and synchronization context after await are the data before await, so there will be no problem in operating the UI after await in cs. If If you need to operate the UI control in the sub-thread, you need to obtain the SynchronizationContext.Current object to obtain the current synchronization context, or use the WinformSynchronizationContext.Current class rewritten by winform to obtain the synchronization context object, and then perform Post or Send operations on the UI control. error.

During the discussion in the WeChat group, the group friends were discussing the issue of cross-thread operation, so they talked about this. Another brother said that creating a control object in a sub-thread and adding it to the form, and then it will be used during operation. Reporting an error, for this, after I tested it, I created a TextBox in the sub-thread, and the main thread assigned a value to the Text, and no error would be reported, and then I guessed that the controls are all inherited from the Control class, which should be the Control class and the SynchronizationContext class I made an association, so although the object created by the sub-thread, it also belongs to the main thread. Then I checked the source code and verified my guess. In the figure below, if we go to the construction method of Contrl() in the sub-thread new TextBox(), and then go to the construction method of internal Control, the parameter autoInstallSyncContext is true,

6dc3c204a7bf61af67ff78bedc4e6a3f.png

Then the WindowsFormSynchronizationContext.InstallIfNeeded() method is called. In this method, we finally see that the control created by the sub-thread finally belongs to the synchronization context of the UI thread. For this reason, I verified it with code.

be788bf916d0c75dbf03b6e80fc333d4.png
5f97758bc6a9640bf8b1664fde733854.png

Execute this code in the code, add a breakpoint in Task.Run, and you can see that before the new TextBox, SynchronizationContext.Current gets null, and then gets the object of WindowsFormsSynchronizationContext, so you can see All Control controls, even if they are all created in sub-threads, still belong to the UI thread.

await AddText();this.Controls.Add(TextBox);
JextBox. Text = "111";
public Task AddText()
{
    var con=WindowsFormsSynchronizationContext.Current;
    return Task.Run(() =>
    {
        var c = SynchronizationContext. Current;
        TextBox = new TextBox();

        var b = SynchronizationContext. Current;
    });
}

Conclusion

For async and await, the deeper level is actually the context flow. Whether to use a new thread is determined by the TaskScheduler, and thread reuse is determined by the ThreadPool. Moreover, asynchronously does not necessarily open a new thread, otherwise the delegate is asynchronous and the control is asynchronous Started a new thread.

Reposted from: look around

Link: cnblogs.com/1996-Chinese-Chen/p/17172773.html

– EOF –

Technical group: Add Xiaobian WeChat dotnet999

Official account: dotnet lecture hall