[C#] async and await continued

Preface

In the article "async and await", we observed some objective rules, but we did not talk about the essence, and there is still a problem left:

In this article, we continue to see how to solve this problem!

Problems left

Let’s look at the code we wrote before:

static public void TestWait2()
{
    
    
     var t = Task.Factory.StartNew(async () =>
     {
    
    
         Console.WriteLine("Start");
         await Task.Delay(3000);
         Console.WriteLine("Done");
     });
     t.Wait();
     Console.WriteLine("All done");
 }

static public void TestWait3()
{
    
    
     var t = Task.Run(async () =>
     {
    
    
         Console.WriteLine("Start");
         await Task.Delay(3000);
         Console.WriteLine("Done");
     });
     t.Wait();
     Console.WriteLine("All done");
}

The question at the time was why Task.Factory.StartNew can see asynchronous effects, but Task.Run has synchronous effects.
That's actually because the t.Wait(); returned by Task.Factory.StartNew does not block the main thread, but the t.Wait(); of Task.Run does.

Then why is Task.Factory.StartNew not stuck?
This is the variable t that should be returned by Task.Factory.StartNew. It is of type Task<Task>!

If Task.Run returns the Task type, if we change it to Task.Factory.StartNew, then the type it returns is Task<Task< int >>

An Unwrap method is provided in .Net4.0 for decoding Task<Task<int>> into Task<int> type, so if the code is changed to:

static public async void Factory嵌套死等()
{
    
    
    Console.WriteLine($"Factory不嵌套死等{
      
      getID()}");
    var t = Task.Factory.StartNew(async() =>
    {
    
    
        Console.WriteLine($"Start{
      
      getID()}");
        await Task.Delay (1000);
        Console.WriteLine($"Done{
      
      getID()}");
    }).Unwrap();
    t.Wait();
    Console.WriteLine($"All done{
      
      getID()}");
}

Then t.Wait(); can also block the main thread at this time.

In fact, Task.Run (introduced in .net4.5) appeared after Task.Factory.StartNew (introduced in .net4.0). Task.Run is to simplify the use of Task.Factory.StartNew.

t.Wait() 和 await t;

Now I analyze the problem from another angle.
Can we achieve asynchronous effect using Task.Run? The answer is yes!
However, we should not use t.Wait(); at this time, but await t;

static public async void Run嵌套Await()
{
    
    
    Console.WriteLine($"Run嵌套Await{
      
      getID()}");
    var t = Task.Run(async () =>
    {
    
    
        Console.WriteLine($"Start{
      
      getID()}");
        await Task.Delay(1000);
        Console.WriteLine($"Done{
      
      getID()}");
    });
    await t;
    Console.WriteLine($"All done{
      
      getID()}");
}

Insert image description here

In this case, the asynchronous effect is achieved.

How await implements asynchronous

Here we can analyze it further.
"1" is the ID of the main thread "5" is the ID of the child thread that the task started.
I found that All done is executed after Done. This should be because await t; "repatriates" the main thread,
and the code after await t; (that is, the printing of the sentence All done) is completed by sub-thread 5.

The whole process is like this. When compiling, the compiler sees that the function uses the async keyword, then the entire function will be converted into a function with a state machine. After decompilation, it is found that the function name becomes MoveNext.

When the main thread executes the sub-function and encounters await, the main thread will return at this time (jumping out of the entire sub-function to execute the next function), and MoveNext will switch the state machine (since the state machine has been switched, the next time MoveNext is When called, execution will proceed downward from await).
However, judging from the phenomenon, the code after await is not called by the main thread, but by the sub-thread of Task. The child thread will call MoveNext again and enter a new state machine.
There is a conclusion here. When the main thread enters a sub-function and encounters an await opportunity, it returns directly from the function. The following code in the function is handed over to the new sub-thread for execution.
To prove this, I wrote another program:

static public async Task AsyncInvoke()
{
    
    
    await Task.Run(() =>
    {
    
    
        Console.WriteLine($"This is 1 ManagedThreadId={
      
      Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    });
    Console.WriteLine($"1{
      
      getID()}");
    await Task.Run(() =>
    {
    
    
        Console.WriteLine($"This is 2 ManagedThreadId={
      
      Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    });
    Console.WriteLine($"2{
      
      getID()}");
    await Task.Run(() =>
    {
    
    
        Console.WriteLine($"This is 3 ManagedThreadId={
      
      Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    });
    Console.WriteLine($"3{
      
      getID()}");
    await Task.Run(() =>
    {
    
    
        Console.WriteLine($"This is 4 ManagedThreadId={
      
      Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    });
    Console.WriteLine($"4{
      
      getID()}");
}

The execution effect is as follows:
Insert image description here
You will find that there are no more awaits in a function. When the main thread encounters an await, it returns and jumps out of this function to execute other functions.
The rest of the await function is completed by the child thread! Multiple awaits are just multiple state machines.
So in a function, if there are multiple awaits, except for the first and subsequent ones, they have nothing to do with the main thread.

A new question arises here, why are the subsequent thread IDs all 5? This is actually not necessarily true. I ran it again:
Insert image description here
this time I found two sub-thread numbers 3 and 5. This is because there is a thread pool behind the Task. Task is translated as task , and simple thread refers to Thread .
After the Task is started, which thread is used is provided by the thread pool behind it, and this thread pool is maintained by .net. Including when the callback occurs, the Task object is notified by a thread in the thread pool! The await operator actually calls ContinueWith of the Task object, so the above code can also be written like this:

/// <summary>
/// 回调式写法
/// </summary>
public void TaskInvokeContinue()
{
    
    
    Task.Run(() =>
    {
    
    
        Console.WriteLine($"This is 1 ManagedThreadId={
      
      Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    }).ContinueWith(t =>
    {
    
    
        Console.WriteLine($"This is 2 ManagedThreadId={
      
      Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    }).ContinueWith(t =>
    {
    
    
        Console.WriteLine($"This is 3 ManagedThreadId={
      
      Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    }).ContinueWith(t =>
    {
    
    
        Console.WriteLine($"This is 4 ManagedThreadId={
      
      Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
    })
    ;
    //不太爽---nodejs---回调式写法,嵌套是要疯掉
}

This further demonstrates that await uses a synchronous method to write asynchronous code.
The reason this is possible is that the function has been transformed into a state machine.

At this point, I have filled in the pitfalls from last time! Next time we will talk about some interesting uses of Task.

summary

I think the most important point is:
the main thread returns when it encounters an await. How to understand this return?
Returning means jumping out of this function, which has nothing to do with this function, and executes other following functions.
The remaining parts of the function after await are completed by the child threads in the thread pool!
Understanding this will help us write asynchronous code!

Updated on July 29, 2023 (a Debug sharing)

I just finished writing this article yesterday, and today I found a problem with a piece of code I wrote before. I didn’t expect to use it so quickly~~ (laughing and crying)

The procedure is roughly like this. I have a main thread with two functions A and B. A and B implement async await.
There is an await tcpcli.SendAsync(str) asynchronous code in A and B. The approximate code is as follows:

while(true)
{
    
    
	await  A(){
    
    
		....
		bool b = await tcpcli.SendAsync(str);
	}
	await  B(){
    
    
		....
		await tcpcli.SendAsync(str);
	}
}

Under normal circumstances, this is no problem. The programs run normally. But when the response from the Tcp service is delayed, problems will arise.
When running to bool b = await tcpcli.SendAsync(str);, according to the previous conclusion, the main thread returns directly, and will directly execute B
and then execute A, but if bool b = await tcpcli.SendAsync(str ); If you are still waiting, the thread will still return.
At this time, a new thread will be opened again, resulting in the concurrency of multiple threads. However, there will be problems if other logical concurrency here (such as writing a Modbus register) .
So, once tcpcli.SendAsync(str) gets stuck, there is something wrong with the logic!

Since logic cannot be concurrent, why didn't I just use synchronization? In fact, the reason was that I didn't know how to get the return value synchronously at the time.
When I called tcpcli.SendAsync(str).Wait();, I found that the return value of Wait() was empty , but I needed the return value, so I used
bool b = await tcpcli.SendAsync(str); In fact, if If you want to get the return value synchronously, you should use:
bool b = tcpcli.SendAsync(str).GetAwaiter().GetResult();
Therefore, finally change the program to:

while(true)
{
    
    
	await  A(){
    
    
		....
		bool b = tcpcli.SendAsync(str).GetAwaiter().GetResult();
	}
	await  B(){
    
    
		....
		tcpcli.SendAsync(str).Wait();
	}
}

Guess you like

Origin blog.csdn.net/songhuangong123/article/details/131980699