C#中async/await的线程ID变化情况

    

一、简单的起步

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


    结果:
        主线程开始ID:1
        主线程结束ID:4        
    
    1、问:async/await会创建新线程吗?
        答:async和await并不会直接创建新的线程,而是通过利用异步机制来实现非阻塞的异步操作。
        
            C#中的async和await关键字并不会创建新的线程。它们实际上是用于异步编程的语法糖。

            当使用async关键字修饰一个方法时,该方法可以被视为一个异步方法。在异步方法内部,可以使用await关键字来等待其他异步操作的完成。

            当遇到await关键字时,异步方法会暂时挂起,让出当前线程的控制权,而不会阻塞线程。当被await的异步操作完成后,异步方法会恢复执行,并返回结果。

            在大多数情况下,异步操作并不会创建新的线程,而是通过利用I/O完成端口或其他异步机制来实现异步操作。这样可以避免创建额外的线程,提高程序的性能和资源利用率。

            注意,如果使用了Task.Run等方法来包装一个同步的阻塞操作,那么它可能会在新的线程上执行。这样做的目的是为了将阻塞操作转换为异步操作,以避免阻塞主线程。


    2、问:异步方法会暂时挂起,是什么意思?
        答:在遇到await Task.Delay(100)时,异步方法会暂时挂起,并让出当前线程的控制权。这里的挂起并不是指线程被挂起或阻塞,而是指异步方法暂时停止执行,并将控制权返回给调用它的线程(主线程)。
        
            当遇到await Task.Delay(100)时,它会创建一个延时任务,该任务会在指定的时间(这里是100毫秒)后完成。然后,异步方法会注册一个回调函数,告诉任务完成后要继续执行下一步。
            在挂起期间,异步方法不会占用线程资源,而是让线程可以执行其他任务。这样可以提高程序的并发性和资源利用率。
            
            一旦延时任务完成,异步方法会被唤醒,并继续执行后续的代码。这时,并不是创建新的线程来执行延时操作,而是通过异步机制来实现非阻塞的延时操作。
            
            具体来说,当异步方法遇到await Task.Delay(100)时,它会将延时任务交给.NET运行时的任务调度器(Task Scheduler)管理。任务调度器会将延时任务放入等待队列中,并继续执行其他任务。
            
            在指定的时间(100毫秒)后,任务调度器会将延时任务标记为完成,并将其添加到就绪队列中。当调度器调度到该任务时,它会通知异步方法继续执行,并返回到原来的线程(主线程)上。
            
            总之,异步方法的挂起并不是线程的挂起或阻塞,而是暂时停止执行,并让出当前线程的控制权。在挂起期间,线程可以执行其他任务。异步方法通过异步机制来实现非阻塞的延时操作,让出当前线程的控制权,并在延时任务完成后继续执行。
        
        
    3、问:遇到await时主线程在做啥,玩泥巴吗?
        答:是的,它不是阻塞,而是去干其它事去了。
        
            当遇到await关键字时,主线程会暂时挂起(挂起点),这并不会阻塞主线程的执行,而是让出当前线程的控制权,允许主线程去执行其他任务。
            
            同时,await关键字会将异步操作交给任务调度器来管理。任务调度器会根据当前的线程池状态和调度策略,将异步操作分配给适当的线程执行。
            
            当异步操作完成后,任务调度器会通知异步方法继续执行。这时,可能会发生线程切换,执行剩下的代码的线程可能是之前执行异步操作的线程,也可能是其他线程。
            
            这种机制使得异步方法能够以非阻塞的方式执行,并允许主线程在等待异步操作完成时继续执行其他任务,提高了程序的并发性和响应性。
            
            注意,异步方法的挂起和恢复是由任务调度器来管理和控制的,具体的线程调度和切换机制是由.NET运行时来处理的。开发人员并不需要显式地关注线程的创建和管理,而是通过使用async和await来编写简洁、清晰的异步代码。

    
    4、问:await也要开线程吧?
        答:不一定,大多数情况下,异步操作并不会创建新的线程,而是利用异步机制(如I/O完成端口)来实现非阻塞的异步操作。
        
            通过使用异步机制,可以将阻塞的I/O操作转换为异步的操作,而不需要创建新的线程。这样可以避免线程的创建和销毁,提高程序的性能和资源利用率。
            
            除了线程池中的线程,调度器也可以使用其他的执行上下文,比如使用事件触发器或计时器来执行异步操作。这种情况下,调度器会将异步操作添加到事件队列或计时器队列中,并在适当的时候触发事件或计时器来执行异步操作。
            
            然而,有些情况下,异步操作可能会创建新的线程。例如:

            (1)使用Task.Run等方法:
            如果使用Task.Run等方法来包装一个同步的阻塞操作,那么它可能会在新的线程上执行。这样做的目的是为了将阻塞操作转换为异步操作,以避免阻塞主线程。

            (2)自定义线程池:
            在某些情况下,开发人员可以自定义线程池来控制异步操作的执行。这可能涉及到线程的创建和管理,以满足特定的需求。

            注意,创建新的线程可能会增系统资源的开销,并且需要进行线程同步和管理。因此,在设计和实现异步操作时,该根据实际情况和需求来选择合适的方式,以平衡性能、资源利用率和代码复杂性。
            
            总结,大多数情况下,异步操作不会创建新的线程,而是利用异步机制来实现非阻塞的操作。但在某些情况下,可能会涉及到创建新的线程来执行异步操作,以满足特定的需求。
    
    
    5、问: await完成后,主线程的ID可能不是1了?
        答:是的。
        
            当遇到await关键字时,主线程会暂时挂起(挂起点),这并不会阻塞主线程的执行,而是让出当前线程的控制权,允许主线程去执行其他任务。
            
            同时,await关键字会将异步操作交给任务调度器来管理。任务调度器会根据当前的线程池状态和调度策略,将异步操作分配给适当的线程执行。
            
            当异步操作完成后,任务调度器会通知异步方法继续执行。这时,可能会发生线程切换,执行剩下的代码的线程可能是之前执行异步操作的线程,也可能是其他线程。
            
            这种机制使得异步方法能够以非阻塞的方式执行,并允许主线程在等待异步操作完成时继续执行其他任务,提高了程序的并发性和响应性。
            
            具体的来说就是:
            当异步操作完成后,任务调度器会通知异步方法继续执行,具体是通过将执行权从之前的线程切换回到原来挂起点的代码,然后继续执行下面的代码。
            
            在异步方法中,遇到await关键字时,会将await之后的代码封装为一个延续(continuation),并注册到异步操作的完成事件上。
            
            当异步操作完成后,任务调度器会将延续添加到就绪队列中,等待调度执行。一旦调度器调度到该延续,它会通知异步方法继续执行,切换回原来的挂起点。
            这个通知是通过线程切换和调度机制实现的。具体来说,任务调度器会选择一个可用的线程(可能是之前执行异步操作的线程,也可能是其他线程),并将执行权转移给该线程。这样,异步方法就可以继续执行await之后的代码。
            
            注意,异步方法的继续执行并不是立即发生的,而是在调度器选择并分配线程之后才会发生。具体的线程调度和切换机制是由.NET运行时和任务调度器来处理的,开发人员不需要显式地管理和控制。
            
            总之,异步操作完成后,任务调度器会通过线程切换和调度机制将执行权切换回原来的挂起点,通知异步方法继续执行下面的代码。这样可以实现非阻塞的异步操作和代码的顺序执行。
    

扫描二维码关注公众号,回复: 16751107 查看本文章

    6、问:那上面的的await应该有答案了吗?
        答:是的,上面可以知道在c处挂起,主线程玩泥巴,这个延时交给任务调度器(不一定是创建线程,也可能是事件回调机制),延时完成后回来,任务调度器会选择一个可用线程(可能是主线程,可能是前面异步操作线程,也有可能是新的其它线程,谁闲谁知道呢,服从领导就OK啦),继续执行c处后面的代码。所以d的ID是随机的,谁也说不准。
    
    
    7、问:什么是上下文?
        答:人话就是,上下文(Context)是指执行代码时所处的环境和状态。它包含了一些与执行相关的信息,如线程调度器、同步上下文、同步上下文流动等。
            比如,你工作的场所,场景,环境。有电脑,笔,桌子,办公室,等等。
    
    
    8、问:所有线程都有上下文?
        答:在异步编程中,线程执行时都会有上下文。上下文提供了执行环境和状态,包括线程的调度、同步上下文、同步上下文流动等。
            人话就是:所有鱼都有自己的生存环境。
    
    
    9、问:上下文的切换都会有消耗资源?
        答:对的。
            切换线程上下文可能会涉及一些开销,包括线程的切换、上下文的保存和恢复等。这是因为不同的线程可能具有不同的执行环境和状态,需要进行一些额外的操作来确保正确的执行。
            人话就是:如果你原来在A处办公,现在调整到B处去办公,你当然需要搬运、布置,打扫等工作,肯定有些许时间的消耗。
        
  


二、再添加一个异步

    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();    


    结果:
        主线程开始ID:1
        异步线程ID开始:3
        异步线程ID结束:3
        主线程结束ID:4    
    a处为主线程ID为1,然后新开一个异步线程task,一闪而过去执行e处,又是一个异步线程但有await。所以f是随机的,可能是1,可能是4,但不可能是3,因为此时3被c处占用。
    对于b和d,因为c是同步线程,所以b和d都是在同一个线程中执行,它们的ID是相同的为3.
    
    
    修改一:将C的10毫秒改变为1000毫秒。

    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();    


    结果:
        主线程开始ID:1
        异步线程ID开始:3
        主线程结束ID:4
        异步线程ID结束:3    
    同样b和d仍然同一线程内,肯定相同,所以两者为3。
    e处之后,f的ID是随机的,但它不可能是3,此是3仍然在c处占用.
    
    
    修改二:把e处改为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


    结果:
        主线程开始ID:1
        异步线程ID开始:3
        异步线程ID结束:3
        主线程结束ID:1    
    bcd处一样为3.
    e处为同步。由主线程执行ID为1,所以后面的f处也为1.
    


三、新加异步中的Await


    1、两个线程,第一个异步中异步,第二同步:

        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    


        结果:
            主线程开始ID:1
            异步线程ID开始:3
            异步线程ID结束:4
            主线程结束ID:1
        e处为同步线程,在主线程中,所以到了f时是主线程。
        b处由Task.Run申请的线程,ID为3,经过c后,原ID为3的挂起,在延时做完后,恢复执行后面代码时,由调度器选择线程来执行后面的d处,所以d的ID是随机的,但不可能是1.
        
        
    2、两个线程,第一个异步中异步,第二次await异步。

        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


        结果: 
            主线程开始ID:1
            异步线程ID开始:3
            异步线程ID结束:5
            主线程结束ID:3    
        b处为异步线程ID为3,经过C处后,d处随机,显示为5.
        e处返回时,b,d使用的线程(3和5)已经返回给线程池,也即线程池是有可能再次给e后面分配1,3,5等,所以这里显示是3.
        
    
    3、修改上面,把延时调整一下,c处占久点

        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


        结果:
            主线程开始ID:1
            异步线程ID开始:3
            主线程结束ID:5
            异步线程ID结束:6
        b处分配ID为3,然后c处等待,在d处随机得到分配的ID,由于它是1000毫秒后,即所有任务都执行完成了,只有它,所以它的随机分配是任何可能,可以是3,5,6等。
        e处过后,返回时由于b处占用了3(哪怕是挂起),所以在f处调度器随机分配线程不可能ID为3,所以上面分配的是5.


    
四、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


    结果:
        主线程开始ID:[1]
        异步线程开始ID:[3]
        异步线程结束ID:[4]
        主线程结束ID:[4]
    上面b处ID结果为3,然后经c处后,在d处随机分配。结果是4.
    f处由于前面await的原因,同样也是随机分配,它是最后执行,所以ID有任意的可能(看调度器的分配了)。
    
    这里,说明的是无论Configiure为True还是False,最后都要以await结束,都要返回到主线程的上下文中,所以它“失效了”.
    

    Task.Run会将异步操作放入线程池中执行,而await会在异步操作完成之前阻塞主线程。当异步操作完成后,会尝试切换回主调线程执行await之后的代码。无论e处的参数是true还是false,异步操作完成后,恢复时都会尝试切换回主调线程执行d处或者f处的代码。
    
    
    
    问:为什么d与f处的线程大多数是一样的?
    答:这个不是绝对的。按优化概率可能是这样。
        调度器通常会尽量将执行权切换回刚完成的异步线程,以继续执行原先挂起的代码。这种方式可以减少线程切换和上下文切换的成本,提高执行效率。
        
        当一个异步任务完成后,调度器会考虑以下几个因素来决定是否将执行权切换回刚完成的异步线程:

        (1)异步线程的可用性:
        如果刚完成的异步线程仍然可用,调度器会优先选择它来执行后续代码,因为这样可以避免线程切换的开销。

        (2)异步线程的负载:
        如果刚完成的异步线程当前正在执行其他任务,调度器可能会选择一个空闲的线程来执行后续代码,以平衡负载。

        (3)上下文切换的成本:
        如果切换到刚完成的异步线程的上下文比切换到其他线程的上下文更低廉,调度器可能会优先选择它来执行后续代码。

        注意,具体的调度策略和行为取决于调度器的实现和配置。不同的调度器可能有不同的优化策略和行为。因此,在实际应用中,可能会出现一些例外情况,导致执行权并不会立即切换回刚完成的异步线程。
    
    
    
    改变一下它的优化,再增加一句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    


    结果:
        主线程开始ID:[1]
        异步线程开始ID:[3]
        异步线程结束ID:[4]
        主线程结束ID:[3]    
    f处变成了3,看来调试器总是在当前比较优闲的ID(大概猜测就是1,3,4中选秀)。
    
    而且,最难得的是大约有10次运行,终于找到一个难得的截图:
  


    再次证明,优化是有规则的,所以中奖概率高,但并非绝对的。
    

猜你喜欢

转载自blog.csdn.net/dzweather/article/details/132818166