Thread ID changes of async/await in C#

    

1. Simple start

    Console.WriteLine($"主线程开始ID:{Thread.CurrentThread.ManagedThreadId}");//a
    await Task.Delay(100);//c
    Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//b


    Result:
        Main thread start ID: 1
        Main thread end ID: 4        
    
    1. Question: Will async/await create a new thread?
        Answer: async and await do not directly create new threads, but use the asynchronous mechanism to achieve non-blocking asynchronous operations.
        
            The async and await keywords in C# do not create new threads. They are actually syntactic sugar for asynchronous programming.

            When a method is modified with the async keyword, the method can be treated as an asynchronous method. Inside an asynchronous method, you can use the await keyword to wait for the completion of other asynchronous operations.

            When encountering the await keyword, the asynchronous method will temporarily suspend and give up control of the current thread without blocking the thread. When the awaited asynchronous operation is completed, the asynchronous method will resume execution and return the result.

            In most cases, asynchronous operations do not create new threads, but implement asynchronous operations by utilizing I/O completion ports or other asynchronous mechanisms. This avoids the creation of additional threads and improves program performance and resource utilization.

            Note that if you use methods such as Task.Run to wrap a synchronous blocking operation, it may be executed on a new thread. The purpose of this is to convert blocking operations into asynchronous operations to avoid blocking the main thread.


    2. Question: What does it mean that the asynchronous method will temporarily hang?
        Answer: When encountering await Task.Delay(100), the asynchronous method will temporarily suspend and give up control of the current thread. Suspension here does not mean that the thread is suspended or blocked, but that the asynchronous method temporarily stops execution and returns control to the thread that called it (the main thread).
        
            When await Task.Delay(100) is encountered, it creates a delay task that will complete after the specified time (here, 100 milliseconds). The asynchronous method then registers a callback function that tells the task to continue to the next step after the task is completed.
            During the suspension period, asynchronous methods do not occupy thread resources, but allow the thread to perform other tasks. This improves program concurrency and resource utilization.
            
            Once the delayed task is completed, the asynchronous method will be awakened and continue executing subsequent code. At this time, instead of creating a new thread to perform the delay operation, the asynchronous mechanism is used to implement the non-blocking delay operation.
            
            Specifically, when the asynchronous method encounters await Task.Delay(100), it will hand over the delayed task to the task scheduler (Task Scheduler) of the .NET runtime for management. The task scheduler will put the delayed task into the waiting queue and continue to execute other tasks.
            
            After the specified time (100 milliseconds), the task scheduler marks the delayed task as complete and adds it to the ready queue. When the scheduler schedules the task, it will notify the asynchronous method to continue execution and return to the original thread (main thread).
            
            In short, the suspension of the asynchronous method is not the suspension or blocking of the thread, but the temporary suspension of execution and giving up control of the current thread. While suspended, the thread can perform other tasks. Asynchronous methods implement non-blocking delayed operations through an asynchronous mechanism, giving up control of the current thread and continuing execution after the delayed task is completed.
        
        
    3. Question: What is the main thread doing when encountering await? Is it playing in the mud?
        Answer: Yes, it is not blocking, but going to do other things.
        
            When encountering the await keyword, the main thread will temporarily suspend (the suspension point). This will not block the execution of the main thread, but will give up control of the current thread and allow the main thread to perform other tasks.
            
            At the same time, the await keyword will hand over asynchronous operations to the task scheduler for management. The task scheduler will allocate asynchronous operations to appropriate threads for execution based on the current thread pool status and scheduling policy.
            
            When the asynchronous operation is completed, the task scheduler will notify the asynchronous method to continue execution. At this time, a thread switch may occur, and the thread executing the remaining code may be the thread that previously performed the asynchronous operation, or it may be another thread.
            
            This mechanism enables asynchronous methods to be executed in a non-blocking manner and allows the main thread to continue performing other tasks while waiting for the asynchronous operation to complete, improving the concurrency and responsiveness of the program.
            
            Note that the suspension and resumption of asynchronous methods are managed and controlled by the task scheduler, and the specific thread scheduling and switching mechanism is handled by the .NET runtime. Developers do not need to explicitly focus on thread creation and management, but can write concise and clear asynchronous code by using async and await.

    
    4. Question: Does await also need to open a thread?
        Answer: Not necessarily. In most cases, asynchronous operations do not create new threads, but use asynchronous mechanisms (such as I/O completion ports) to implement non-blocking asynchronous operations.
        
            By using the asynchronous mechanism, blocking I/O operations can be converted into asynchronous operations without creating new threads. This can avoid the creation and destruction of threads and improve program performance and resource utilization.
            
            In addition to threads in the thread pool, the scheduler can also use other execution contexts, such as event triggers or timers to perform asynchronous operations. In this case, the scheduler will add the asynchronous operation to the event queue or timer queue, and trigger the event or timer at the appropriate time to perform the asynchronous operation.
            
            However, there are situations where asynchronous operations may create new threads . For example:

            (1) Use methods such as Task.Run:
            If you use methods such as Task.Run to wrap a synchronous blocking operation, it may be executed on a new thread. The purpose of this is to convert blocking operations into asynchronous operations to avoid blocking the main thread.

            (2) Custom thread pool:
            In some cases, developers can customize the thread pool to control the execution of asynchronous operations. This may involve the creation and management of threads to meet specific needs.

            Note that creating new threads may increase the overhead of system resources and requires thread synchronization and management. Therefore, when designing and implementing asynchronous operations, it is important to choose the appropriate approach based on the actual situation and needs to balance performance, resource utilization, and code complexity.
            
            In summary, in most cases, asynchronous operations do not create new threads, but use the asynchronous mechanism to achieve non-blocking operations. But in some cases, it may involve creating new threads to perform asynchronous operations to meet specific needs.
    
    
    5. Question: After await is completed, the ID of the main thread may not be 1?
        Answer: Yes.
        
            When encountering the await keyword, the main thread will temporarily suspend (the suspension point). This will not block the execution of the main thread, but will give up control of the current thread and allow the main thread to perform other tasks.
            
            At the same time, the await keyword will hand over asynchronous operations to the task scheduler for management. The task scheduler will allocate asynchronous operations to appropriate threads for execution based on the current thread pool status and scheduling policy.
            
            When the asynchronous operation is completed, the task scheduler will notify the asynchronous method to continue execution. At this time, a thread switch may occur, and the thread executing the remaining code may be the thread that previously performed the asynchronous operation, or it may be another thread.
            
            This mechanism enables asynchronous methods to be executed in a non-blocking manner and allows the main thread to continue performing other tasks while waiting for the asynchronous operation to complete, improving the concurrency and responsiveness of the program.
            
            Specifically:
            when the asynchronous operation is completed, the task scheduler will notify the asynchronous method to continue execution, specifically by switching the execution right from the previous thread back to the code at the original suspension point, and then continue to execute the following code.
            
            In an asynchronous method, when the await keyword is encountered, the code after await will be encapsulated as a continuation and registered to the completion event of the asynchronous operation.
            
            When the asynchronous operation is completed, the task scheduler will add the continuation to the ready queue, waiting for scheduled execution. Once the scheduler schedules the continuation, it notifies the asynchronous method to continue execution and switch back to the original suspension point.
            This notification is implemented through thread switching and scheduling mechanisms. Specifically, the task scheduler selects an available thread (it may be the thread that previously performed the asynchronous operation, or it may be another thread) and transfers execution rights to that thread. In this way, the asynchronous method can continue executing the code after await.
            
            Note that the continuation of the asynchronous method does not happen immediately, but occurs after the scheduler selects and allocates a thread. The specific thread scheduling and switching mechanism is handled by the .NET runtime and task scheduler, and developers do not need to explicitly manage and control it.
            
            In short, after the asynchronous operation is completed, the task scheduler will switch the execution right back to the original suspension point through the thread switching and scheduling mechanism, and notify the asynchronous method to continue executing the following code. This enables non-blocking asynchronous operations and sequential execution of code.
    

    6. Question: Is there an answer to the await above?
        Answer: Yes, it can be known from the above that it hangs at c and the main thread plays in the mud. This delay is handed over to the task scheduler (not necessarily the creation thread, but may also be the event callback mechanism). After the delay is completed, it comes back and the task is scheduled. The processor will select an available thread (it may be the main thread, it may be the previous asynchronous operation thread, or it may be another new thread, who knows, just obey the leader) and continue to execute the code behind point c. So the ID of d is random and no one can tell for sure.
    
    
    7. Question: What is context?
        Answer: In human terms, context refers to the environment and state in which code is executed. It contains some execution-related information, such as thread scheduler, synchronization context, synchronization context flow, etc.
            For example, the place, scene, and environment where you work. There are computers, pens, desks, offices, etc.
    
    
    8. Question: Do all threads have context?
        Answer: In asynchronous programming, there is a context when threads execute. The context provides the execution environment and status, including thread scheduling, synchronization context, synchronization context flow, etc.
            As the saying goes: all fish have their own living environment.
    
    
    9. Question: Does context switching consume resources?
        Answer: Right.
            Switching thread contexts may involve some overhead, including thread switching, context saving and restoration, etc. This is because different threads may have different execution environments and states, requiring some additional operations to ensure correct execution.
            As the saying goes: If you originally worked in location A and now move to location B, of course you need to move, arrange, clean, etc., which will definitely consume some time.
        
  


2. Add another asynchronous

    Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
    Task task = Task.Run(() =>
    {
        Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
        Thread.Sleep(10);//c
        Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
    });
    await Task.Delay(100);//e
    Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f
    Console.ReadKey();    


    Result:
        Main thread start ID: 1
        Asynchronous thread ID start: 3
        Asynchronous thread ID end: 3
        Main thread end ID: 4    
    The main thread ID at a is 1, then open a new asynchronous thread task, and execute e in a flash , another asynchronous thread but with await. So f is random, it may be 1, it may be 4, but it cannot be 3, because 3 is occupied by c at this time.
    For b and d, because c is a synchronized thread, b and d are executed in the same thread, and their IDs are the same as 3.
    
    
    Modification 1: Change C's 10 milliseconds to 1000 milliseconds.

    Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
    Task task = Task.Run(() =>
    {
        Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
        Thread.Sleep(1000);//c
        Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
    });
    await Task.Delay(100);//e
    Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f
    Console.ReadKey();    


    Result:
        Main thread start ID: 1
        Asynchronous thread ID start: 3
        Main thread end ID: 4
        Asynchronous thread ID end: 3    
    Similarly, b and d are still in the same thread and must be the same, so both are 3.
    After e, the ID of f is random, but it cannot be 3. This is because 3 is still occupied at c.
    
    
    Modification 2: Change e to thread.Sleep(1000)

    Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
    Task task = Task.Run(() =>
    {
        Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
        Thread.Sleep(10);//c
        Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
    });
    Thread.Sleep(1000);//e
    Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f


    Result:
        Main thread start ID: 1
        Asynchronous thread ID start: 3
        Asynchronous thread ID end: 3
        Main thread end ID: 1    
    bcd is the same as 3.
    e is synchronous. The execution ID of the main thread is 1, so the subsequent f is also 1.
    


3. Await in newly added asynchronous


    1. Two threads, the first is asynchronous and the second is synchronous:

        Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
        Task task = Task.Run(async () =>
        {
            Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
            await Task.Delay(10);//c
            Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
        });
        Thread.Sleep(1000);//e
        Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f    


        Result:
            Main thread start ID: 1
            Asynchronous thread ID start: 3
            Asynchronous thread ID end: 4
            Main thread end ID: 1
        The synchronization thread at e is in the main thread, so it is the main thread when f is reached.
        The thread applied by Task.Run at b has an ID of 3. After c, the original ID of 3 is suspended. After the delay is completed, when the subsequent code is resumed, the scheduler selects the thread to execute the subsequent d. , so the ID of d is random, but it cannot be 1.
        
        
    2. Two threads, the first one is asynchronous and the second one is await asynchronous.

        Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
        Task task = Task.Run(async () =>
        {
            Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
            await Task.Delay(10);//c
            Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
        });
        await Task.Delay(1000);//e
        Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f


        Result: 
            Main thread start ID: 1
            Asynchronous thread ID start: 3
            Asynchronous thread ID end: 5
            Main thread end ID: 3 The    
        asynchronous thread ID at b is 3. After passing through C, d is random and displayed as 5.
        At e When returning, the threads (3 and 5) used by b and d have been returned to the thread pool, that is, the thread pool may allocate 1, 3, 5, etc. to the back of e again, so the display here is 3. 3. Modify the above
        
    
    , Adjust the delay and make position c longer.

        Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
        Task task = Task.Run(async () =>
        {
            Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
            await Task.Delay(1000);//c
            Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
        });
        await Task.Delay(10);//e
        Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f


        Result:
            Main thread start ID: 1
            Asynchronous thread ID start: 3
            Main thread end ID: 5
            Asynchronous thread ID end: 6
        The assigned ID at b is 3, then wait at c, and get the assigned ID randomly at d, since it is After 1000 milliseconds, that is, all tasks have been executed, only it, so its random allocation is any possibility, it can be 3, 5, 6, etc.
        After e, when returning, since b occupies 3 (even if it is suspended), it is impossible for the scheduler to randomly allocate the thread ID to 3 at f, so the one allocated above is 5.


    
4. Invalidation of Configureawait

    Console.WriteLine($"主线程开始ID:[{Environment.CurrentManagedThreadId}]");//a
    await Task.Run(async () =>
    {
        Console.WriteLine($"异步线程开始ID:[{Environment.CurrentManagedThreadId}]");//b
        await Task.Delay(1000);//c
        Console.WriteLine($"异步线程结束ID:[{Environment.CurrentManagedThreadId}]");//d
    }).ConfigureAwait(true);//e
    Console.WriteLine($"主线程结束ID:[{Environment.CurrentManagedThreadId}]");//f


    Result:
        Main thread start ID: [1]
        Asynchronous thread start ID: [3]
        Asynchronous thread end ID: [4]
        Main thread end ID: [4]
    The ID result at b above is 3, and then after c, at d randomly assigned. The result is 4.
    Due to the previous await, position f is also randomly assigned. It is executed last, so the ID has any possibility (depending on the allocation of the scheduler).
    
    Here, it is explained that whether Configiure is True or False, it must end with await and return to the context of the main thread, so it "fails".
    

    Task.Run will put the asynchronous operation into the thread pool for execution, while await will block the main thread before the asynchronous operation is completed. When the asynchronous operation is completed, it will try to switch back to the main calling thread to execute the code after await. Regardless of whether the parameter at e is true or false, after the asynchronous operation is completed, it will try to switch back to the main calling thread to execute the code at d or f during recovery.
    
    
    
    Question: Why are most of the threads at d and f the same?
    Answer: This is not absolute. According to the optimization probability, this may be the case.
        The scheduler will usually try to switch execution rights back to the asynchronous thread that just completed to continue executing the originally suspended code. This approach can reduce the cost of thread switching and context switching and improve execution efficiency.
        
        When an asynchronous task is completed, the scheduler will consider the following factors to decide whether to switch execution rights back to the asynchronous thread that just completed:

        (1) Availability of asynchronous threads:
        If the asynchronous thread that just completed is still available, the scheduler will give priority to it to execute subsequent code, because this can avoid the overhead of thread switching.

        (2) Load of asynchronous threads:
        If the asynchronous thread that has just completed is currently executing other tasks, the scheduler may choose an idle thread to execute subsequent code to balance the load.

        (3) Cost of context switching:
        If switching to the context of the just-completed asynchronous thread is cheaper than switching to the context of other threads, the scheduler may prefer it to execute subsequent code.

        Note that the specific scheduling policies and behaviors depend on the scheduler implementation and configuration. Different schedulers may have different optimization strategies and behaviors. Therefore, in actual applications, there may be some exceptions that cause the execution right to not be immediately switched back to the asynchronous thread that just completed.
    
    
    
    Change its optimization and add an await:

    Console.WriteLine($"主线程开始ID:[{Environment.CurrentManagedThreadId}]");//a
    await Task.Run(async () =>
    {
        Console.WriteLine($"异步线程开始ID:[{Environment.CurrentManagedThreadId}]");//b
        await Task.Delay(1000);//c
        Console.WriteLine($"异步线程结束ID:[{Environment.CurrentManagedThreadId}]");//d
    }).ConfigureAwait(true);//e
    await Task.Delay(1000);
    Console.WriteLine($"主线程结束ID:[{Environment.CurrentManagedThreadId}]");//f    


    Result:
        Main thread start ID: [1]
        Asynchronous thread start ID: [3]
        Asynchronous thread end ID: [4]
        Main thread end ID: [3]    
    f becomes 3. It seems that the debugger always compares the current Youxian's ID (roughly guessed to be the 1st, 3rd, and 4th pick).
    
    And, the most rare thing is that after about 10 runs, I finally found a rare screenshot:
  


    Proving once again that optimization has rules, so the probability of winning is high, but not absolute.
    

Guess you like

Origin blog.csdn.net/dzweather/article/details/132818166