多线程之旅:解读async和await
发布日期:2022-04-11 08:52:47 浏览次数:10 分类:博客文章

本文共 13805 字,大约阅读时间需要 46 分钟。

早上无意中看到了async和await关键字,花了十几分钟看了一下msdn,大概明白了是什么一个东西,和大家分享一下。

await关键字的中文是期待的意思。在我们编程中想表达“我待会期待这里会有一个值,但我不是现在就要,我先去做其他事情,你完成的时候告诉我”。其实异步模式非常符合现实中场景,现实生活中还真的很少东西是同步的。等车的时候没事干可以拿手机出来玩一下,首发下邮件,而不是直愣愣的干在那里等着车过来。

话说回来,在C# 5中借助await可以更好的辅助我们进行异步编程模式的开发,通过改变执行流程,使得异步执行看起来更像同步执行。

一个async方法里通常包含一个或多个的对应的await操作符,但如果没有 await表达式也不会导致编译错误。但如果调用一个async方 法,却不使用await关键字来标记一个挂起点的话,程序将会忽略async关键字并以同步的方式执行。编译器会对类似的问题发出警告。

我直接拿和图来举例吧。

 

public partial class MainWindow : Window    {        private async void StartButton_Click(object sender, RoutedEventArgs e)        {            int contentLength = await AccessTheWebAsync();            resultsTextBox.Text +=                String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength);        }        async Task
AccessTheWebAsync() { HttpClient client = new HttpClient(); Task
getStringTask = client.GetStringAsync("http://msdn.microsoft.com"); DoIndependentWork(); string urlContents = await getStringTask; return urlContents.Length; } void DoIndependentWork() { resultsTextBox.Text += "Working . . . . . . .\r\n"; } }

 可以从图中看出,和正常的执行流程是不一样的,我们甚至还会回到“看似已经执行完的方法中”,接着await下面执行

 

但其实这也不难理解,如果你用过yield return的话。可以看做执行到await的时候(箭头第6步),方法就返回了。等await后面的动作执行完以后我们再回来继续执行下面的步骤。

也就是说,await 表达式标记一个点,在该点上直到等待的异步操作完成方法才能继续。 同时,方法挂起,并且返回到方法的调用方。

值得注意的是,返回并不意味着方法已经执行完成,也就是方法还没退出。await 表达式中异步方法的挂起不能使该方法退出,并且 finally 块不会运行。

但天有不测风云,壮士有可能一去不复返,改一下StartButton_Click代码,看下面的这个情况

1         private void StartButton_Click(object sender, RoutedEventArgs e)2         {3             int contentLength = AccessTheWebAsync().Result;4 5             resultsTextBox.Text +=6                 String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength);7         }

 我们调用了task的Result,这个属性内部会调用await的方法,也就是会阻塞线程,也就是说线程执行到第3行的时候,就会被阻塞了(阻塞线程是罪大恶极的事情,要牢记在心)。一直等待AccessTheWebAsync()方法完成。

但是AccessTheWebAsync()能完成吗?答案是不能!因为AccessTheWebAsync方法需要依赖这个已经被阻塞的线程回去完成。好吧,一个互相等待的死锁出现了。

 

你妈妈喊你回家吃饭,快点来接着执行await下面的代码

 

 

我靠,我被Result定住了,没办法回去了。

 

。。。。。。。。

不指定SynchronizationContext
要解决这个问题,就要知道为什么会产生这种情况。

要产生这种情况需要满足两个条件,

1.我指定了要接下来执行下面代码的SynchronizationContext

 2.SynchronizationContext里面包含的所有线程被阻塞

 

SynchronizationContext是什么东西呢?简单来说就是.NET提供的一个类,能让任务在指定的线程里面运行。详细的话看我的这篇

一些SynchronizationContext只封装了单独的一个线程,比如UI thead, 还有一些封装的是一组线程,比如说线程池,可以挑选任意一个线程池的线程来完成指定的任务。

所以说为啥这里悲剧了?因为UI thread所在的SynchronizationContexts只有它这么一个独苗,一旦它被阻塞,就没辙了。

所以解决这种问题的方式有这些,一个就是在await 指定的时候,选择”哥不care你那个线程回来执行,反正来线程啊!“

1         async Task
AccessTheWebAsync() 2 { 3 4 HttpClient client = new HttpClient(); 5 6 Task
getStringTask = client.GetStringAsync("http://msdn.microsoft.com"); 7 8 DoIndependentWork(); 9 10 string urlContents = await getStringTask.ConfigureAwait(false)
;11             return urlContents.Length;12         }
第10行新加的.ConfigureAwait(false) 表达的就是这个意思,也就是我不在乎你接下来执行的线程是什么线程,来自哪个SynchronizationContext。默认是true,也就是我指定就要原来的那个SynchronizationContext,其他的宁可死也不要。

第二种方式就是让它原来的SynchronizationContext本身就封装了多个线程,这样即使阻塞了一个线程,也可以调用其他线程来完成任务。

 
int contentLength = Task.run(AccessTheWebAsync()).Result;
觉得这样写会改变了原来的调用的话,也可以把这层封装在AccessTheWebAsync方法里,这样外部调用的方式可以保持不变。
 
async Task
AccessTheWebAsync() { return Task.Run(async ()=> {          HttpClient client = new HttpClient();   Task
getStringTask = client.GetStringAsync("http://msdn.microsoft.com"); DoIndependentWork(); string urlContents = await getStringTask; return urlContents.Length; }) }

 
好吧,如果你十分钟没看懂的话,那咱们来看看好玩的yield return,再回来看看这个await。

为什么yield return返回的是IEnumerable<T>类型?就是因为IEnumerable表达的意思是“我只关心你当前的和下一个,我不管你总共有多少”,就和数学归纳法一样,有第一个(n=1),有下一个(n+1),就能推断出所有情况。所以当我们foreach遍历一个IEnumerable类型的时候,无论是在foreach之前就计算好所有元素,还是调用一次MoveNext时才计算并返回一个,都符合这个意思。如果我们的输入序列和输出序列都是用yield return来实现的,那么就相当于构造了一条延迟执行的pipeline。恩,没错,和Linq的实现一样的道理。

 

C#编译器是通过一个状态机来实现的,每次调用MoveNext方法return 出去之前,都修改当前的状态,使得下次进入的时候是从其他状态进入。

class Foo    {        public IEnumerable
AnIterator() { yield return "1"; yield return "2"; yield return "3"; } } class Program { static void Main() { var collection = new Foo(); foreach (var s in collection.AnIterator()) { Console.WriteLine(s); } } }

通过reflactor查看编译器帮我们做的事情,我们每写一个yield return就对应产生一个分支的case

private bool MoveNext()        {            switch (this.<>1__state)            {                case 0:                    this.<>1__state = -1;                    this.<>2__current = "1";                    this.<>1__state = 1; //修改当前状态为1                    return true;                case 1:                    this.<>1__state = -1;                    this.<>2__current = "2";                    this.<>1__state = 2; //修改当前状态为2                    return true;                case 2:                    this.<>1__state = -1;                    this.<>2__current = "3";                    this.<>1__state = 3; //修改当前状态为3                    return true;                case 3:                    this.<>1__state = -1;                    break;            }            return false;        }

 

 其实Jeffrey Richter以前就借助过yield return实现的状态机来简化异步编程,使得它看起来像同步编程。

下面的代码yield return 1看起来是不是很像是await?

private static IEnumerator
PipeServerAsyncEnumerator(AsyncEnumerator ae) { // Each server object performs asynchronous operations on this pipe using (var pipe = new NamedPipeServerStream( "Echo", PipeDirection.InOut, -1, PipeTransmissionMode.Message, PipeOptions.Asynchronous | PipeOptions.WriteThrough)) { // Asynchronously accept a client connection pipe.BeginWaitForConnection(ae.End(), null); yield return 1; // A client connected, let's accept another client var aeNewClient = new AsyncEnumerator(); aeNewClient.BeginExecute(PipeServerAsyncEnumerator(aeNewClient), aeNewClient.EndExecute); // Accept the client connection pipe.EndWaitForConnection(ae.DequeueAsyncResult()); // Asynchronously read a request from the client Byte[] data = new Byte[1000];    pipe.BeginRead(data, 0, data.Length, ae.End(), null); yield return 1; // The client sent us a request, process it. Int32 bytesRead = pipe.EndRead(ae.DequeueAsyncResult()); // My sample server just changes all the characters to uppercase // But, you can replace this code with any compute-bound operation data = Encoding.UTF8.GetBytes( Encoding.UTF8.GetString(data, 0, bytesRead).ToUpper().ToCharArray()); // Asynchronously send the response back to the client pipe.BeginWrite(data, 0, data.Length, ae.End(), null);      yield return 1; // The response was sent to the client, close our side of the connection pipe.EndWrite(ae.DequeueAsyncResult()); } // Close happens in a finally block now!}

 

 终于,Jeffrey Richter在C#5里面终于用关键字await更好的实现了他的想法(他去年来上海宣讲的时候说await是他创建的)

有兴趣的话可以看看await创建的状态机长啥样干了什么
一段这样的代码,

private static async Task
MyMethodAsync(Int32 argument) { Int32 local = argument; try { Type1 result1 = await Method1Async(); for (Int32 x = 0; x < 3; x++) { Type2 result2 = await Method2Async(); } } catch (Exception) { Console.WriteLine("Catch"); } finally { Console.WriteLine("Finally");  } return "Done";}

 

Type1 result1 = await Method1Async();  经过编译以后,会成这样

// This is the state machine method itselfvoid IAsyncStateMachine.MoveNext() {   String result = null;   // Task's result value   // Compiler­inserted try block ensures the state machine’s task completes   try {      // Assume we're logically leaving the 'try' block      // If 1st time in state machine method,      // execute start of original method      Boolean executeFinally = true;      if (m_state == ­1) {         m_local = m_argument;      }      // Try block that we had in our original code      try {         TaskAwaiter
awaiterType1; TaskAwaiter
awaiterType2; switch (m_state) { case ­1: // Start execution of code in 'try' // Call Method1Async and get its awaiter awaiterType1 = Method1Async().GetAwaiter(); if (!awaiterType1.IsCompleted) { m_state = 0; // 'Method1Async' is completing asynchronously m_awaiterType1 = awaiterType1; // Save the awaiter for when we come back           // Tell awaiter to call MoveNext when operation completes m_builder.AwaitUnsafeOnCompleted(ref awaiterType1, ref this); // The line above invokes awaiterType1's OnCompleted which approximately calls ContinueWith(t => MoveNext()) on the Task being awaited. executeFinally = false; // We're not logically leaving the 'try' block return; // Thread returns to caller } // 'Method1Async' completed synchronously break; case 0: // 'Method1Async' completed asynchronously awaiterType1 = m_awaiterType1; // Restore most­recent awaiter break;     case 1: // 'Method2Async' completed asynchronously awaiterType2 = m_awaiterType2; // Restore most­recent awaiter goto ForLoopEpilog;   }        //下面省略的是loop部分和异常处理部分,分开看比较容易看懂
        .....      }   m_builder.SetResult(result);}

 

恩,实现咋看上去有点复杂,不过注释也已经写得挺明白了,基本上关键就是。

m_builder.AwaitUnsafeOnCompleted(ref awaiterType1, ref this) invokes awaiterType1's OnCompleted which approximately calls ContinueWith(t => MoveNext()) on the Task being awaited.

然后就return出去了,接着在task完成之后再跳转回来。基本原理和上面的类似。

这里是loop部分的代码

// After the first await, we capture the result & start the 'for' loop                  m_resultType1 = awaiterType1.GetResult(); // Get awaiter's result               ForLoopPrologue:                  m_x = 0;          // 'for' loop initialization                  goto ForLoopBody; // Skip to 'for' loop body               ForLoopEpilog:                  m_resultType2 = awaiterType2.GetResult();                  m_x++;            // Increment x after each loop iteration                  // Fall into the 'for' loop’s body               ForLoopBody:                  if (m_x < 3) {  // 'for' loop test                     // Call Method2Async and get its awaiter                     awaiterType2 = Method2Async().GetAwaiter();                     if (!awaiterType2.IsCompleted) {                        m_state = 1;                   // 'Method2Async' is completing asynchronously                        m_awaiterType2 = awaiterType2; // Save the awaiter for when we come back                        // Tell awaiter to call MoveNext when operation completes                        m_builder.AwaitUnsafeOnCompleted(ref awaiterType2, ref this);                        executeFinally = false;        // We're not logically leaving the 'try' block                        return;                        // Thread returns to caller                     }                     // 'Method2Async' completed synchronously                     goto ForLoopEpilog;  // Completed synchronously, loop around               }

 

 完整的代码

void IAsyncStateMachine.MoveNext() {   String result = null;         try {      Boolean executeFinally = true;      if (m_state == ­1) {         m_local = m_argument;      }            try {         TaskAwaiter
awaiterType1; TaskAwaiter
awaiterType2; switch (m_state) { case ­1: awaiterType1 = Method1Async().GetAwaiter(); if (!awaiterType1.IsCompleted) { m_state = 0; m_awaiterType1 = awaiterType1; m_builder.AwaitUnsafeOnCompleted(ref awaiterType1, ref this); executeFinally = false; return; } break; case 0: awaiterType1 = m_awaiterType1; break;     case 1: awaiterType2 = m_awaiterType2; goto ForLoopEpilog; } m_resultType1 = awaiterType1.GetResult(); ForLoopPrologue: m_x = 0; goto ForLoopBody; ForLoopEpilog: m_resultType2 = awaiterType2.GetResult(); m_x++; ForLoopBody: if (m_x < 3) { awaiterType2 = Method2Async().GetAwaiter(); if (!awaiterType2.IsCompleted) { m_state = 1; m_awaiterType2 = awaiterType2; m_builder.AwaitUnsafeOnCompleted(ref awaiterType2, ref this); executeFinally = false; return; } goto ForLoopEpilog; } } catch (Exception) { Console.WriteLine("Catch"); } finally { if (executeFinally) { Console.WriteLine("Finally"); } } result = "Done"; } catch (Exception exception) { m_builder.SetException(exception); return; } m_builder.SetResult(result);}
View Code

 

 

好吧,我承认十分钟学会有点标题党,但是看到这里,我想大家基本都有了个印象吧,目的达到了。

转载地址:https://www.cnblogs.com/lwzz/archive/2013/05/13/3076386.html 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:多线程之旅:避免死锁——简单的锁分级(锁排序)
下一篇:多线程之旅六——异步编程模式,自己实现IAsyncResult

发表评论

最新留言

第一次来,支持一个
[***.219.124.196]2024年04月22日 11时09分41秒