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



继续(15)的例子

一、ConfigureAwait()的作用

  private async void BtnAsync_Click(object sender, EventArgs e)//异步{Stopwatch sw = Stopwatch.StartNew();TxtInfo.Clear();AppendLine("异步检索开始...");AppendLine($"当前线程Id:{Environment.CurrentManagedThreadId}");//bint idx = 0;foreach (var b in Data.Books){string t = await Task.Run(b.Search).ConfigureAwait(false);//aAppendLineThread($"{  idx}.{t}--线程Id:{Environment.CurrentManagedThreadId}");//c}sw.Stop();AppendLineThread($"异步检索完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");} 



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、例子界面





代码:

  public Form1(){InitializeComponent();}private readonly StringBuilder strResult = new StringBuilder();private void Test_ConfigureAwait(object sender, EventArgs e){Stopwatch sw = Stopwatch.StartNew();string s1 = cbAwait.Checked.ToString();string s2 = cbConfigureAwait.Checked.ToString();strResult.Clear();strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】主线程开始:Await:{s1},ConfigureAwait:{s2}");ChildMethod();strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】主线程开始等待");Thread.Sleep(3000);sw.Stop();strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】主线程结束{sw.ElapsedMilliseconds}ms");MessageBox.Show(Owner, "主线程结束,输出结果", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);Print(strResult.ToString());}private async void ChildMethod(){strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】ChildMethod开始......");Stopwatch sw = Stopwatch.StartNew();if (cbAwait.Checked){await Task.Run(() =