今天周五,早上起床晚了。赶着挤公交上班。可是目前眼前有这么几件事情。刷牙洗脸、泡牛奶、煎蛋。在同步编程眼中。先刷牙洗脸,而后烧水泡牛奶。再煎蛋,最后喝牛奶吃蛋。毫无疑问,在时间紧促的当下。它完了,稳的迟到、半天工资没了。那么异步编程眼中,或许还有一丝解救的但愿。先烧水,同时刷牙洗脸。而后泡牛奶,等牛奶不那么烫的时候煎个蛋。最后喝牛奶吃蛋。也许还能不迟到。在本篇文章中将围绕这个事例讲解异步编程。html
在看异步模式以前咱们先看一个同步调用的事例:编程
class Program { private const string url = "http://www.cninnovation.com/"; static void Main(string[] args) { AsyncTest(); } public static void AsyncTest() { Console.WriteLine(nameof(AsyncTest)); using (var client=new WebClient()) { string content = client.DownloadString(url); Console.WriteLine(content.Substring(0,100)); } Console.WriteLine(); } }
在这个事例中,DownloadString方法将请求的地址下载为string资源,可是在咱们实际运行当中,由于DownloadString方法阻塞调用线程,直到返回结果。整个程序就一直卡在了DownloadString方法这里。这样的体验是很是的不愉快的。有了问题,天然也就有了对应的解决方法,下面咱们就一块儿来看看对应的解决方法的进步史吧。c#
异步模式是处理异步特性的第一种方式,它不只可使用几个API,还可使用基本功能(如委托类型)。不过这里须要注意的是在使用.NET Core调用委托的这些方法时,会抛出一个异常,其中包含平台不支持的信息。数组
异步模式定义了BeginXXX方法和EndXXX方法。例如上面同步方法是DownloadString,那么异步就是BeginDownloadString和EndDownloadString方法。BeginXXX方法接收其同步方法的全部输入的参数,EndXXX方法使用同步方法全部的输出参数,并按照同步方法的返回类型来返回结果。BeginXXX定义了一个AsyncCallback参数,用于接受在异步方法执行完成后调用的委托。BeginXXX方法返回IAsyncResult,用于验证调用是否已经完成,而且一直等到方法执行结束。服务器
咱们看下异步模式的事例,由于上面事例中的WebClient没有异步模式的实现,这里咱们使用WebRequest来代替:网络
class Program { private const string url = "http://www.cninnovation.com/"; static void Main(string[] args) { AsyncTest(); } public static void AsyncTest() { Console.WriteLine(nameof(AsyncTest)); WebRequest request = WebRequest.Create(url); IAsyncResult result = request.BeginGetResponse(ReadResponse, null); Console.ReadLine(); void ReadResponse(IAsyncResult ar) { using (WebResponse response = request.EndGetResponse(ar)) { Stream stream = response.GetResponseStream(); var reader = new StreamReader(stream); string content = reader.ReadToEnd(); Console.WriteLine(content.Substring(0, 100)); Console.WriteLine(); } } } }
上面事例中展示了异步调用的一种方式---使用异步模式。先使用WebRequest类的Create方法建立WebRequest,而后使用BeginGetResponse方法异步将请求发送到服务器。调用线程没有被阻塞。第一个参数上面有讲,完成后回调的委托。一旦网络请求完成,就会调用该方法。框架
在UI应用程序中使用异步模式有一个问题:回调的委托方法没有在UI线程中容许,所以若是不切换到UI,就不能访问UI元素的成员,而是抛出一个异常。调用线程不能访问这个对象,由于另外一个线程拥有它。为了简化这个过程在.NET Framework 2.0 中引入了基于时间的异步模式,这样更好的解决了此问题,下面就介绍基于事件的异步模式。异步
基于事件的异步模式定义了一个带有”Async”后缀的方法。下面看下如何使用这个基于事件的异步模式,仍是使用的第一个事例进行修改。async
class Program { private const string url = "http://www.cninnovation.com/"; static void Main(string[] args) { AsyncTest(); } public static void AsyncTest() { Console.WriteLine(nameof(AsyncTest)); using (var client =new WebClient()) { client.DownloadStringCompleted += (sender, e) => { Console.WriteLine(e.Result.Substring(0,100)); }; client.DownloadStringAsync(new Uri(url)); Console.ReadLine(); } } }
在上述事例中,对于同步方法DownloadString,提供了一个异步变体方法DownloadStringAsync。当请求完成时会触发DownloadStringCompleted 事件,关于事件使用及描述前面文章已有详细介绍了。这个事件类型一共带有两个参数一个是object类型,一个是DownloadStringCompletedEventArgs类型。后面个这个类型经过Result属性返回结果字符串。异步编程
这里使用的DownloadStringCompleted 事件,事件处理成将经过保存同步上下文的线程来调用,在应用程序中这就是UI线程,所以能够直接访问UI元素。这里就是与上面那个异步模式相比更优之处。下面咱们看看基于事件的异步模式进一步的改进将是什么样的————基于任务的异步模式。
在.NET Framework 4.5中更新了WebClient类,也新增提供了基于任务的异步模式,该模式也定义了一个”Async”后缀的方法,返回一个Task类型,可是因为基于事件的异步模式已经采用了,因此更改成——DownloadStringTaskAsync。
DownloadStringTaskAsync方法声明返回为Task<string>,可是不须要一个Task<string>类型的变量接收返回结果,只须要声明一个string类型的变量。而且使用await关键字。此关键字会解除线程的阻塞,去完成其余的任务。咱们看下面这个事例
class Program { private const string url = "http://www.cninnovation.com/"; static async Task Main(string[] args) { await AsyncTestTask(); } public static async Task AsyncTestTask() { Console.WriteLine("当前任务Id是:"+Thread.CurrentThread.ManagedThreadId); Console.WriteLine(nameof(AsyncTestTask)); using (var client = new WebClient()) { string content = await client.DownloadStringTaskAsync(url); Console.WriteLine("当前任务Id是:"+Thread.CurrentThread.ManagedThreadId); Console.WriteLine(content.Substring(0,100)); Console.ReadLine(); } } }
上面代码相对于以前的就较为简单多了,而且也没有阻塞,不用切换回UI线程。调用顺序也和同步方法同样。
这里我单独的放出了容许结果,新增了当前任务显示,在刚进入方法时任务为1,可是执行完成DownloadStringTaskAsync方法后,任务id变成了8,上面其余的事例容许此代码也都是返回任务id为1,这也就是基于任务的异步模式的不一样点。
async和await关键字编译器功能,编译器会用Task类建立代码。若是不使用这两个关键字,也是能够用c#4.0Task类的方法来实现一样的功能,虽然会麻烦点。下面咱们看下async和await这两个关键字能作什么,如何采用简单的方式建立异步方法,如何并行调用多个异步方法等等。
这里咱们首先建立一个观察线程和任务的方法,来更好的观察理解发送的变化。
public static void SeeThreadAndTask(string info) { string taskinfo = Task.CurrentId == null ? "没任务" : "任务id是:" + Task.CurrentId; Console.WriteLine($"{info} 在线程{Thread.CurrentThread.ManagedThreadId}和{taskinfo}中执行"); }
同时准备了一个同步方法,该方法使用Delay方法等待一段时间后返回一个字符串。
static void Main(string[] args) { var name= GetString("张三"); Console.WriteLine(name); } static string GetString(string name) { SeeThreadAndTask($"运行{nameof(GetString)}"); Task.Delay(3000).Wait(); return $"你好,{name}"; }
上面咱们也说了不使用哪两个关键字也可使用Task类实现一样的功能,这里咱们采用一个简单的作大,使用Task.Run方法返回一个任务。
static void Main(string[] args) { SeeThreadAndTask($"运行{nameof(Main)}"); var name= GetStringAsync("张三"); Console.WriteLine(name.Result); Console.ReadLine(); } static Task<string> GetStringAsync(string name) => Task.Run<string>(() => { SeeThreadAndTask($"运行{nameof(GetStringAsync)}"); return GetString(name); });
咱们继续来看await和async关键字,使用await关键字调用返回任务的异步方法,可是也须要使用async修饰符。
static void Main(string[] args) { SeeThreadAndTask($"运行{nameof(Main)}"); GetSelfAsync("张三"); Console.ReadLine(); } private static async void GetSelfAsync(string name) { SeeThreadAndTask($"开始运行{nameof(GetSelfAsync)}"); string result =await GetStringAsync(name); Console.WriteLine(result); SeeThreadAndTask($"结束运行{nameof(GetSelfAsync)}"); }
在异步方法完成前,该方法内的其余代码不会执行。可是,启动GetSelfAsync方法的线程能够被重用。该线程没有被阻塞。
这里刚开始时候中是没有任务执行的,GetStringAsync方法开始在一个任务中执行,这里所在的线程也是不一样的。其中GetString和GetStringAsync方法都执行完毕,等待以后返回如今GetStringAsync开始转变为线程3,同时也没有任务。await确保任务完成后继续执行,可是如今使用的是另外一个线程。这一个行为在咱们使用控制台应用程序和具备同步上下文的应用程序之间是不一样的。
能够对任何提供GetAwaiter方法并对awaiter的对象async关键字。其中awaiter用OnCompleted方法实现INotifyCompletion接口,完成任务时调用,下面事例中没有使用await关键字,而是使用GetAwaiter方法,返回一个TaskAwaiter,而且使用OnCompleted方法,分配一个在任务完成时调用的本地函数。
static void Main(string[] args) { SeeThreadAndTask($"运行{nameof(Main)}"); GetSelfAwaiter("张三"); Console.ReadLine(); } private static void GetSelfAwaiter(string name) { SeeThreadAndTask($"运行{nameof(GetSelfAwaiter)}"); TaskAwaiter<string> awaiter = GetStringAsync(name).GetAwaiter(); awaiter.OnCompleted(OnCompletedAwauter); void OnCompletedAwauter() { Console.WriteLine(awaiter.GetResult()); SeeThreadAndTask($"运行{nameof(GetSelfAwaiter)}"); } }
咱们看这个运行结果,再与上面调用异步方法的运行结果进行对比,好像相似于使用await关键字的情形。至关于编译器把await关键字后面的全部的代码放进OnCompleted方法的代码块中完成。固然也可另外方法使用GetAwaiter方法。
static void Main(string[] args) { SeeThreadAndTask($"运行{nameof(Main)}"); GetSelfAwaiter("张三"); Console.ReadLine(); } private static void GetSelfAwaiter(string name) { SeeThreadAndTask($"运行{nameof(GetSelfAwaiter)}"); string awaiter = GetStringAsync(name).GetAwaiter().GetResult(); Console.WriteLine(awaiter); SeeThreadAndTask($"运行{nameof(GetSelfAwaiter)}"); }
这里咱们介绍使用Task对象的特性来处理任务的延续。GetStringAsync方法返回一个Task<string>对象包含了任务建立的一些信息,并一直保存到任务完成。Task类的ContinueWith定义了完成任务以后就调用的代码。这里指派给ContinueWith方法的委托接收将已完成的任务做为参数传入,可使用Result属性访问任务的返回结果。
static void Main(string[] args) { SeeThreadAndTask($"运行{nameof(Main)}"); GetStringContinueAsync("张三"); Console.ReadLine(); } /// <summary>
/// 使用ContinueWith延续任务 /// </summary>
/// <param name="name"></param>
private static void GetStringContinueAsync(string name) { SeeThreadAndTask($"开始 运行{nameof(GetStringContinueAsync)}"); var result = GetStringAsync(name); result.ContinueWith(t=> { string answr = t.Result; Console.WriteLine(answr); SeeThreadAndTask($"结束 运行{nameof(GetStringContinueAsync)}"); }); }
这里咱们观察运行结果能够发如今执行完成任务后继续执行ContinueWith方法。其中这个方法在线程4和任务2中完成。这里至关于又开始了一个新的任务,也就是使用ContinueWith方法对任务进行必定的延续。
在每一个异步方法中能够调用一个或多个异步方法。那么如何进行编码呢?这就看这些异步方法之间是否存在相互依赖了。
正常来讲按照顺序调用:
static void Main(string[] args) { SeeThreadAndTask($"运行{nameof(Main)}"); ManyAsyncFun(); Console.ReadLine(); } private static async void ManyAsyncFun() { var result1 = await GetStringAsync("张三"); var result2 = await GetStringAsync("李四"); Console.WriteLine($"第一我的是{result1},第二我的是{result2}"); }
使用await关键字调用每一个异步方法。若是一个异步方法依赖另外一个异步方法的话,那么这个await关键字就比较有效,可是若是第二个异步方法独立于第一个异步方法,这样能够不使用await关键字,这样的话整个ManyAsyncFun方法将会更快的返回结果。
还一种状况,异步方法不依赖于其余异步方法,并且不使用await,而是把每一个异步方法的返回结果赋值给Task比变量,这样会运行的更快。组合器能够帮助实现这一点,一个组合器能够接受多个同一类型的参数,并返回同一类型的值。若是任务返回相同的类型,那么该类型的数组也可用于接收await返回的结果。当只有等待全部任务都完成时才能继续完成其余的任务时,WhenAll方法就有实际用途,当调用的任务在等待完成时任何任务都能继续完成任务的时候就能够采用WhenAny方法,它可使用任务的结果继续。
static void Main(string[] args) { SeeThreadAndTask($"运行{nameof(Main)}"); ManyAsyncFunWithWhenAll(); Console.ReadLine(); } private static async void ManyAsyncFunWithWhenAll() { Task<string> result1 = GetStringAsync("张三"); Task<string> result2 = GetStringAsync("李四"); await Task.WhenAll(result1, result2); Console.WriteLine($"第一我的是{result1.Result},第二我的是{result2.Result}"); }
在使用await依次调用两个异步方法时,诊断会话6.646秒,采用WhenAll时,诊断会话话费3.912秒,能够看出速度明显提升了。
C#带有更灵活的await关键字:它如今能够等待任何提供GetAwaiter方法的对象。下面咱们讲一个可用于等待的新类型-----ValueTask,与Task相反,ValueTask是一个结构。这具备性能优点,因ValueTask在堆上没有对象。
static async Task Main(string[] args) { SeeThreadAndTask($"运行{nameof(Main)}"); for (int i = 0; i < 10000; i++) { string result2 = await GetStringDicAsync("张三"); } Console.WriteLine("结束"); Console.ReadLine(); } private readonly static Dictionary<string, string> names = new Dictionary<string, string>(); private static async ValueTask<string> GetStringDicAsync(string name) { if (names.TryGetValue(name,out string result)) { return result; } else { result = await GetStringAsync(name); names.Add(name,result); return result; } }
上面事例中咱们使用ValueTask替代了Task,由于咱们前面讲,每次使用Task都会对内存进行分配空间,在咱们反复时会形成必定的性能上的损耗,可是使用ValueTask只会存放在Stack中,存放实际值而不是记忆地址。
并不是全部的.NET Framework的全部的类都引用了新的异步方法,在使用框架中不一样的类的时候会发现,还有许多类只提供了BeginXXX方法和EndXXX方法的异步模式,没有提供基于任务的异步模式,可是咱们能够把异步模式更改成基于任务的异步模式。
提供的Task.Factory.FromAsync<>泛型方法,将异步模式转换为基于任务的异步模式。
static void Main(string[] args) { ConvertingAsync(); Console.ReadLine(); } private static async void ConvertingAsync() { HttpWebRequest request = WebRequest.Create("http://www.cninnovation.com/") as HttpWebRequest; using (WebResponse response = await Task.Factory.FromAsync<WebResponse>(request.BeginGetResponse(null,null),request.EndGetResponse)) { Stream stream = response.GetResponseStream(); using (var reader=new StreamReader(stream)) { string content = reader.ReadToEnd(); Console.WriteLine(content.Substring(0,100)); } } }
上一节咱们讲了错误和异常处理,可是咱们在使用异步方法时,应该知道一些特殊的处理方式,咱们先看一个简单的事例
static void Main(string[] args) { Dont(); Console.WriteLine("结束"); Console.ReadLine(); } static async Task ThrowAfterAsync(int ms, string msg) { await Task.Delay(ms); throw new Exception(msg); } private static void Dont() { try { ThrowAfterAsync(200,"第一个错误"); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
在这个事例中,调用了异步方法,可是并无等待,try/catch就捕获不到异常,这是由于Dont方法在抛出异常前就运行结束了。
那么异步方法的异常怎么处理呢,有一个较好的方法就是使用await关键字。将其放在try/catch中,异步方法调用完后,Dont方法就会释放线程,但它会在任务完成时保持任务的引用。
private static async void Dont() { try { await ThrowAfterAsync(200,"第一个错误"); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
那么多个异步方法调用,每一个都抛出异常怎么处理呢?咱们看下面事例中
private static async void Dont() { try { await ThrowAfterAsync(200,"第一个错误"); await ThrowAfterAsync(100, "第二个错误"); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
调用两个异步方法,可是都抛出异常,由于捕获了一个异常以后,try块代码就没有继续调用第二方法,也就只抛出了第一个异常
private static async void Dont() { try { Task t1 = ThrowAfterAsync(200, "第一个错误"); Task t2 = ThrowAfterAsync(100, "第二个错误"); await Task.WhenAll(t1,t2); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
对上述事例修改,采用并行调用两个方法,在2s秒后第一个抛出异常,1s秒后第二个异常也抛出了,使用Task.WhenAll,不论是否抛出异常,都会等两个任务完成。所以就算捕获了第一个异常也会执行第二个方法。可是咱们只能看见抛出的第一个异常,没有显示第二个异常,可是它存在在列表中。
这里为了获得全部失败任务的异常信息,看将Task.WhenAll返回的结果写到一个Task变量中。这个任务会一个等到全部任务结束。
private static async void Dont() { Task taskResult = null; try { Task t1 = ThrowAfterAsync(200, "第一个错误"); Task t2 = ThrowAfterAsync(100, "第二个错误"); await (taskResult=Task.WhenAll(t1,t2)); } catch (Exception ex) { Console.WriteLine(ex.Message); foreach (var item in taskResult.Exception.InnerExceptions) { Console.WriteLine(item.Message); } } }
这里能够访问外部任务的Exception属性了。Exception属性是AggregateException类型的。这里使用Task.Exception.InnerExceptions属性,它包含了等待中全部的异常列表。这样就能够轻松的变量全部的异常了。
本篇文章介绍了三种不一样的异步模式,同时也介绍 了相关的异步编程基础。如何对应的去使用异步方法大有学问,用的好的异步编程减小性能消耗,提升运行效率。可是使用很差的异步编程提升性能消耗,下降运行效率也不是不可能的。这里也只是简单的介绍了异步编程的相关基础知识以及错误处理。更深更完美的编程模式还得实践中去探索。异步编程使用async和await关键字等待这些方法。而不会阻塞线程。异步编程的介绍到这里就暂时结束,下一篇文章咱们将详细介绍反射、元数据。
不是井里没有水,而是你挖的不够深。不是成功来得慢,而是你努力的不够多。
欢迎你们扫描下方二维码,和我一块儿学习更多的C#知识