• (16)线程的实例认识:Await,Async,ConfigureAwait



        继续(15)的例子

    一、ConfigureAwait()的作用

    1.         private async void BtnAsync_Click(object sender, EventArgs e)//异步
    2.         {
    3.             Stopwatch sw = Stopwatch.StartNew();
    4.             TxtInfo.Clear();
    5.             AppendLine("异步检索开始...");
    6.             AppendLine($"当前线程Id:{Environment.CurrentManagedThreadId}");//b
    7.             int idx = 0;
    8.             foreach (var b in Data.Books)
    9.             {
    10.                 string t = await Task.Run(b.Search).ConfigureAwait(false);//a
    11.                 AppendLineThread($"{++idx}.{t}--线程Id:{Environment.CurrentManagedThreadId}");//c
    12.             }
    13.             sw.Stop();
    14.             AppendLineThread($"异步检索完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
    15.         }    


        
        1、上面a后面添加的ConfigureAwait(false)是什么意思?
            ConfigureAwait(true)和ConfigureAwait(false)也是用于配置async/await操作的,它们用于控制异步操作在await之后是否在原始的上下文中继续执行。
            当ConfigureAwait(true)时,异步操作在await之后会返回到原始的上下文中(一般是调用方线程或UI线程)继续执行。
            当ConfigureAwait(false)时,异步操作在await之后会在非原始的上下文中(一般指当前的异步线程)继续执行。
            
            理解例子(2):
            假设你是一个餐厅的经理,你需要安排服务员去执行一些任务。服务员是你的线程,而任务是异步操作。你可以选择两种不同的方式来安排服务员执行任务。
            当你使用ConfigureAwait(true)时,这就像你让服务员在原始的上下文中执行任务。这意味着服务员会在你所在的位置继续执行任务。例如,如果你在前台接待顾客,然后遇到了一个异步任务(比如接听电话),你可以选择让服务员在你身边继续执行任务,这样你接听完电话后可以让他立即继续处理顾客。
            而当你使用ConfigureAwait(false)时,这就像你让服务员离开原始的上下文去执行任务。这意味着服务员会离开你的位置去执行任务。例如,如果你在前台接待顾客,然后遇到了一个异步任务(比如处理支付),你可以选择让服务员"离开你的位置"去处理支付,这样你可以继续接待其他顾客。
            所以,ConfigureAwait(true)让异步操作在原始的上下文中继续执行,就像让服务员在你身边继续执行任务。而ConfigureAwait(false)让异步操作在非原始的上下文中继续执行,就像让服务员离开你的位置去执行任务。


        2、为true与false的好处?
            ConfigureAwait(true)的好处:
            保留当前的上下文环境:在某些情况下,你可能需要在异步操作执行完毕后回到原始的上下文环境,例如,你在UI线程上调用了一个异步操作,然后在操作完成后需要更新UI。使用ConfigureAwait(true)可以确保异步操作在原始的上下文中继续执行,由于要在异步操作中进行线程切换,所以有上下文恢复的开销。
            简化代码:如果你确信异步操作不会引发线程上下文相关的问题,并且想要保持在原始的上下文中执行,那么使用ConfigureAwait(true)可以简化代码,避免了显式指定ConfigureAwait(false)的需要。
            
            ConfigureAwait(false)的好处:
            提高性能:如果你的异步操作不需要回到原始的上下文环境,并且没有对UI或特定上下文的依赖,使用ConfigureAwait(false)可以在异步操作中避免不必要的线程切换和上下文恢复的开销,从而提高性能。
            避免死锁:在某些情况下,当异步操作依赖于特定的上下文环境时,使用ConfigureAwait(false)可以避免出现死锁的可能性。例如,在UI线程上使用ConfigureAwait(true)可能导致异步操作在等待UI线程资源时出现死锁,因为UI线程正在等待异步操作完成。
            总体来说,ConfigureAwait(true)适用于需要保留原始上下文环境的情况,可以避免线程切换和上下文恢复的开销,并简化代码。而ConfigureAwait(false)适用于不需要回到原始上下文环境的情况,可以提高性能并避免死锁。
            注意,使用ConfigureAwait(false)也意味着您要确保在异步操作中不使用与UI线程上下文相关的资源或数据。否则,可能会导致线程安全问题或其他错误。

        
        3、UI线程与异步线程可以是同一个线程吗?
            UI线程与异步线程并不是绝对的不一样,它们类似对象,可以同时指向同一个线程,比如UI线程可以指向UI线程本身,异步线程也可以在同时指向UI线程。
            因此,UI线程和异步线程可以同时指向同一个实际线程。
            UI线程和异步线程实际上是线程的角色或标识(变量名),用于区分它们在应用程序中的不同任务和行为。虽然它们可以在某些情况下指向同一个线程,但它们通常用于不同的目的和上下文。

            UI线程通常负责用户界面的呈现、响应用户输入以及处理UI事件。异步线程一般用于执行耗时的操作,以避免阻塞UI线程,以及在后台执行任务或处理并发操作。
            虽然可以出现UI线程和异步线程指向同一个线程的情况,但仍然需要考虑线程间的上下文切换和线程安全性。UI线程和异步线程在相应的上下文中进行任务处理,以确保正确的执行和交互。

            总之,UI线程和异步线程可以共享同一个线程对象,但它们在应用程序中具有不同的角色和任务。
        
        
        4、true与false的效果
            上例的task与上下文无关,所以用true或false都不会有多大的影响。但我们可以查看一下线程ID的变化:
            
            
            
            左边为true,异步线程y操作a处task.run后,根据true的设置,控制权就会将线程y交还线程池(让线程池进行管理y,是释放还是利用,都与现在的无关了),然后,控制权切换恢复到原始上下文(即UI线程),这时就是UI线程在执行了,以确保后续的代码在UI线程上执行。因此,b处是UI线程在执行,c处也是UI线程在执行(UI线程委托UI自己做事),因此,c处的线程ID与UI线程的线程ID相同。上面ID都为1。
            注意:
            在部分编程框架和操作系统中,UI线程的ID可能被预先分配为1。注意,这个结果是特定环境下的表现,并不适用于所有的编程框架和操作系统。在其他环境中,UI线程的ID可能有不同的分配规则或方式。因此,在编写代码时,最好避免依赖特定环境的线程ID分配方式,而是使用提供的API或方法来获取线程ID。
            
            右边为false,异步线程y操作在a处task.run后,根据false的设置,线程y不会交还给线程池,也不会尝试恢复到原始上下文(例如切换到UI线程),控制操作权仍然在线程y中紧紧把握,然后线程y就当家做主,继续执行b处下面的代码,这个异步线程y是由task.run时线程池智能分配的,所以每一个task.run对应一个异步线程,c也由这个异步线程在执行,所以c处因为线程池的分配而显示的异步线程ID是随机的,可能相同可能不同。所以在b是ID是1,在c处随机由线程池决定。
            当为false,在c后面如果操作UI控件,比如TxtInfo.AppendText="1111";将会出错。因为false后,返回的线程只能处理与UI无关的事,结果现在处理TxtInfo,将引发异常。上面代码能正常是因为后面全是委托AppendLintThread。

    二、Await/Async


        1、例子界面
            
            


            
            


            
            代码:

    1.         public Form1()
    2.         {
    3.             InitializeComponent();
    4.         }
    5.         private readonly StringBuilder strResult = new StringBuilder();
    6.         private void Test_ConfigureAwait(object sender, EventArgs e)
    7.         {
    8.             Stopwatch sw = Stopwatch.StartNew();
    9.             string s1 = cbAwait.Checked.ToString();
    10.             string s2 = cbConfigureAwait.Checked.ToString();
    11.             strResult.Clear();
    12.             strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】主线程开始:Await:{s1},ConfigureAwait:{s2}");
    13.             ChildMethod();
    14.             strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】主线程开始等待");
    15.             Thread.Sleep(3000);
    16.             sw.Stop();
    17.             strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】主线程结束{sw.ElapsedMilliseconds}ms");
    18.             MessageBox.Show(Owner, "主线程结束,输出结果", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
    19.             Print(strResult.ToString());
    20.         }
    21.         private async void ChildMethod()
    22.         {
    23.             strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】ChildMethod开始......");
    24.             Stopwatch sw = Stopwatch.StartNew();
    25.             if (cbAwait.Checked)
    26.             {
    27.                 await Task.Run(() =>
    28.                 {
    29.                     strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程开始......");
    30.                     Thread.Sleep(2000);
    31.                     strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程延时2000ms结束");
    32.                 }).ConfigureAwait(cbConfigureAwait.Checked);
    33.             }
    34.             else
    35.             {
    36.                 Task.Run(() =>
    37.                 {
    38.                     strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程开始......");
    39.                     Thread.Sleep(2000);
    40.                     strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程延时2000ms结束");
    41.                 }).ConfigureAwait(cbConfigureAwait.Checked);
    42.             }
    43.             sw.Stop();
    44.             strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】ChildMethod结束{sw.ElapsedMilliseconds}ms");
    45.         }
    46.         private void Print(string s)
    47.         {
    48.             txtInfo.AppendText(s + $"{Environment.NewLine}");
    49.             txtInfo.ScrollToCaret(); txtInfo.Refresh();
    50.         }
    51.         private void BtnPrint_Chick(object sender, EventArgs e)
    52.         { Print(strResult.ToString()); }


            
        
        2、在C#中,使用`await`关键字可以实现异步执行,并且在等待异步结果返回时不阻塞当前线程,而是将控制权交还给调用方。通常情况下,这个调用方可以是UI线程,但也可以是其他线程。当调用方遇到`await`关键字时,它会暂停执行并允许其他代码继续执行,不会阻塞线程。

            在执行到`await`关键字时,异步操作将开始执行,并且调用方将继续执行该关键字后面的代码。当异步操作完成并返回结果时,调用方将恢复执行且可以处理异步操作的结果。无论调用方在`await`之前还是之后结束,都不会影响异步操作的执行。

            需要注意的是,当使用`await`时,调用方必须在某种异步上下文中,例如使用异步方法、异步事件处理程序或通过`Task.Run`等方法创建异步操作。这样才能正确地管理和调度异步操作,并使其在适当的时候恢复执行。

            总结起来,`await`关键字可以使代码在等待异步结果返回时不被阻塞,并将控制权交还给调用方,以便它可以继续执行其他代码。调用方可以是UI线程或其他线程,而执行的顺序将取决于异步操作的完成时间。
        
        
        3、上面什么都不选择时
            主方法先调用子方法,由于Task.Run是异步,所以子方法中一闪而过直接执行最下面的子方法结束信息,至于task.run让它自行2秒后添加信息,而这期间,主方法也是只要一调用子方法就不管它,也直接执行到延时3秒处,所以当子方法延时2秒,稍后主方法的延时3秒也到期了,后面就添加主方法结束的信息。
            
            当只选择Await时,
            主方法调用子方法后,也是自行继续向下执行。子方法遇到await Task.run就需要阻塞执行等待2秒后,因为configureawait没选中,为false,所以task.run后面的代码仍然由异步线程直接继续执行下去,直到子方法的信息追加完成,当然肯定比主方法的延时3秒更早地追加信息,所以最后显示的还是主方法结束的信息。
        
        
        
        3、再一次看一下ConfigureAwait的效果
            
            


            
            
            


            
            
            当选中cbAwait和cbConfigureAwait时。
            主方法调用子方法,进入await task.run用异步线程进行异步操作,当它完成时因为configureawait为真,即这个异步线程y必须交权,需要切换到调用者或UI线程上,即主方法的线程上去,这里主方法线程UI线程正在延时3秒处,没有空闲,线程y就要一直等它空闲,直到在messagebox.show时得到UI线程的空间,于是异步线程y就回线程池去了,而正在弹出信息框进的UI线程得到空间,就返回到子方法中继续向下执行,直到子方法最后的信息执行完成后,就返回到主方法中,继续向下,也就是print把信息打印出来。所以看到的信息是,主方法信息都完成了,最后才是子方法的信息。
            
            
            问:为什么说在messagebox.show得到了空闲呢?
            答:为了观察是什么时间追加的信息,我们做下面的修改:

    1.         private void Test_ConfigureAwait(object sender, EventArgs e)
    2.         {
    3.             Stopwatch sw = Stopwatch.StartNew();
    4.             string s1 = cbAwait.Checked.ToString();
    5.             string s2 = cbConfigureAwait.Checked.ToString();
    6.             strResult.Clear();
    7.             strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】主线程开始:Await:{s1},ConfigureAwait:{s2}");
    8.             ChildMethod();
    9.             strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】主线程开始等待");
    10.             Thread.Sleep(3000);
    11.             sw.Stop();
    12.             strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】主线程结束{sw.ElapsedMilliseconds}ms");
    13.             strResult.AppendLine($"对话框前:{DateTime.Now.TimeOfDay}");
    14.             MessageBox.Show(Owner, "主线程结束,输出结果", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
    15.             strResult.AppendLine($"对话框后:{DateTime.Now.TimeOfDay}");
    16.             Print(strResult.ToString());
    17.         }
    18.         private async void ChildMethod()
    19.         {
    20.             strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】ChildMethod开始......");
    21.             Stopwatch sw = Stopwatch.StartNew();
    22.             if (cbAwait.Checked)
    23.             {
    24.                 await Task.Run(() =>
    25.                 {
    26.                     strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程开始......");
    27.                     Thread.Sleep(2000);
    28.                     strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程延时2000ms结束");
    29.                 }).ConfigureAwait(cbConfigureAwait.Checked);
    30.             }
    31.             else
    32.             {
    33.                 Task.Run(() =>
    34.                 {
    35.                     strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程开始......");
    36.                     Thread.Sleep(2000);
    37.                     strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程延时2000ms结束");
    38.                 }).ConfigureAwait(cbConfigureAwait.Checked);
    39.             }
    40.             strResult.AppendLine($"子方法延时前{DateTime.Now.TimeOfDay}");
    41.             Thread.Sleep(3000);
    42.             strResult.AppendLine($"子方法延时后{DateTime.Now.TimeOfDay}");
    43.             sw.Stop();
    44.             strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】ChildMethod结束{sw.ElapsedMilliseconds}ms");
    45.         }


        
            得出的结果是:
            


            
            可以看到异步线程在信息框时得到空闲,从而完成切换到UI中,UI中就到子方法中继续完成剩下的代码,这里面包括特意加了一个3秒的延时,它也得到了执行,直到子方法全部完成才回到了主方法中去打印print。
            
        
        
        

  • 相关阅读:
    为什么qt设置了utf-8 bom 格式后还是有乱码
    一、多线程的基本概念
    Java 反射设置List属性
    最详解消息队列以及RabbbitMQ之HelloWorld
    分享从零开始学习网络设备配置--2.4 利用三层交换机实现部门间网络互访
    Spring与Redis的整合&&Redis注解式缓存以及Redis雪崩等问题的解决
    有了InheritableThreadLocal为啥还需要TransmittableThreadLocal?
    电子证据的固定方法研究
    Java_移位运算简述
    LitCTF2023 - Reverse方向 全WP
  • 原文地址:https://blog.csdn.net/dzweather/article/details/132656741