继续(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(() =>{strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程开始......");Thread.Sleep(2000);strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程延时2000ms结束");}).ConfigureAwait(cbConfigureAwait.Checked);}else{Task.Run(() =>{strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程开始......");Thread.Sleep(2000);strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程延时2000ms结束");}).ConfigureAwait(cbConfigureAwait.Checked);}sw.Stop();strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】ChildMethod结束{sw.ElapsedMilliseconds}ms");}private void Print(string s){txtInfo.AppendText(s + $"{Environment.NewLine}");txtInfo.ScrollToCaret(); txtInfo.Refresh();}private void BtnPrint_Chick(object sender, EventArgs e){ 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得到了空闲呢?
答:为了观察是什么时间追加的信息,我们做下面的修改:
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");strResult.AppendLine($"对话框前:{DateTime.Now.TimeOfDay}");MessageBox.Show(Owner, "主线程结束,输出结果", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);strResult.AppendLine($"对话框后:{DateTime.Now.TimeOfDay}");Print(strResult.ToString());}private async void ChildMethod(){strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】ChildMethod开始......");Stopwatch sw = Stopwatch.StartNew();if (cbAwait.Checked){await Task.Run(() =>{strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程开始......");Thread.Sleep(2000);strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程延时2000ms结束");}).ConfigureAwait(cbConfigureAwait.Checked);}else{Task.Run(() =>{strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程开始......");Thread.Sleep(2000);strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】子线程延时2000ms结束");}).ConfigureAwait(cbConfigureAwait.Checked);}strResult.AppendLine($"子方法延时前{DateTime.Now.TimeOfDay}");Thread.Sleep(3000);strResult.AppendLine($"子方法延时后{DateTime.Now.TimeOfDay}");sw.Stop();strResult.AppendLine($"【{Environment.CurrentManagedThreadId}】ChildMethod结束{sw.ElapsedMilliseconds}ms");}
得出的结果是:
可以看到异步线程在信息框时得到空闲,从而完成切换到UI中,UI中就到子方法中继续完成剩下的代码,这里面包括特意加了一个3秒的延时,它也得到了执行,直到子方法全部完成才回到了主方法中去打印print。