【c#基础】异步编程
1:使用异步编程,方案调用是在后台运行(通常在线程或任务的帮助下),并且不会阻塞调用线程。
3中不同模式的异步编程:异步模式、基于事件的异步模式和基于任务的异步模式(Task-based Asynchronous Pattern TAP)。TAP是利用async和await关键字来实现。
主要内容:延续任务和同步上下文。如何取消正在执行的任务。如果后台任务执行时间较长,就有可能需要取消任务。
委托类型也实现了异步模式。
一:异步模式
通常异步模式定义了BeginXXX方法和Endxxx方法,如一个同步方法DownloadString,其异步版本就是BeginDownloadString和EndDownloadString方法。
BeginXXX方法接口其同步方法的所有输入参数,EndXXX方法四通同步方法的所有输出参数,并按照同步方的放回类型来返回结果。使用异步 模式时,BeginXXX方法还定义了一个AsyncResult,用于接受在异步方法执行完成后调用的委托。
BeginXXX方法返回IAsyncResult,用于验证调用是否已经完成,并且一直等到方法的执行结束。
WebClient类没有提供异步模式的实现方式,但是可以用HttpWebRequst类来替代,因为该类通过BeginGetResponse和EndGetResponse方法提供这种模式。
二:基于时间的异步模式
OnAsyncEventPattern方法使用了基于事件的异步模式。这个模式由WebClient类实现。
基于事件的异步模式定义了一个带有“Async”后缀的方法。
如同步方法DownloadString WebClient类提供一个异步变体方法DownloadStringAsync。
异步方法完成时,不是定义要调用的委托,而是定义了一个事件。
基于事件的异步模式的优势在于易于使用。
在自定义类中实现异步模式的一种方式是使用BackgroundWorker类来实现异步调用同步方法。
BackergroundWorker类实现了基于事件的异步模式。
这样使代码更加简单,但是与同步方法调用相比,顺序颠倒了。调用异步方法之前,需要定义这个方法完成时发生什么。
三:基于任务的异步模式
.Net4.5中,更新了WebClient类,提供了基于任务的异步模式(TAP)。这个模式定义了一个带有Async后缀的方法,并返回一个Task类型。由于WebClient类已经提供了另一个带Async后缀的方法来实现基于任务的异步模式,因此新方法名为DownloadStringTaskAsync.
DownloadStringTaskAsync方法声明3为返回Task<string>。但是,不需要声明一个Task<string>类型的变量来设置DownloadStringTaskAsync方法返回的记过。只要声明一个String类型的变量,并使用await关键字。await关键字会解除线程的阻塞,完成其他任务, 当DownloadStringTaskAsync方法完成其后台处理后,就可以从后台任务获取结果。
string resp=await client.DownloadStringTaskAsync();
async 关键字创建了一个状态机,类似于yield return 语句
HttpClient类是在.Net4.5中新添加的类。使用GetAsync方法发出一个异步Get请求。
然后要读取内容,需要另一个异步方法。ReadAsStringAsync方法返回字符串格式的内容。
var client=new HttpClinet(参数); var response=await client.GetAsync(url); string resp=await response.Content.ReadAsStringAsync();
要利用同步功能创建后台任务,可以使用Task.Run方法。传递给Task.Run方法的代码块在后台线程上运行。
四:异步编程的基础
ansync和await关键字知识编译器功能。编译器会用Task类创建代码。如果不使用async和await关键字可以用Task类方法来实现同样的功能。
static string Greeting(string name) { Task.Delay(300).Wait(); return $"Hello,{name}"; } static Task<string> GreetingAsync(string name) { return Task.Run(()=>Greeting(name)); }
五:调用异步方法
使用await关键字来第暗涌返回任务的异步方法。使用关键字await关键字需要用async修饰符声明方法。在GreetingAsync方法完成前,该方法内的其他代码不会继续执行。但是启动CallerWithAsync方法的线程可以被重用。该线程没有阻塞。
private static async void CallerWithAsync() { string result = await GreetingAsync("Stephanie"); Console.WriteLine(result); }
async修饰符只能用于返回Task或void的方法。程序的入口点,即Main方法不能使用async修饰符。await只能用于返回Task的方法。
六:延续任务
GreetingAsync方法返回一个Task<string>对象。该Task<string>对象包含任务创建的信息,并保存到任务完成。Task类的ContinueWith方法定义了任务完成后就调用的代码。
指派给ContinueWith方法的委托接收将已完成的任务作为参数传入,使用Result属性可以访问任务返回的结果。
private static void CallerWithContinuationTask() { Task<string> t1 = GreetingAsync("Stephanie"); t1.ContinueWith(t => { string result = t1.Result; Console.WriteLine(result); }); }
编译器把await关键字后的所有代码放进ContinueWith方法的代码块中来转换成await关键字。
其实就是我们在使用await时,下面的代码编译器会给我们编译进到ContinueWith方法代码块中。
例子:
string result = await GreetingAsync("Stephanie"); Console.WriteLine(result);
中的Console.WriteLine(result);
编译器编译成了下图ContinueWiht中的代码块。
t1.ContinueWith(t => { string result = t1.Result; Console.WriteLine(result); });
七:同步上下文
如果验证方法找那个使用的线程,会发现CallerWithAsync方法和CallerWithContinuationTask方法。在方法的不同生命阶段使用了不同的线程。一个线程用于调用GreetingAsync方法,另一个线程执行await关键字后面的代码,或者继续执行ContinueWith方法内的代码块。
如果调用异步方法的线程分配给了同步上下文,await完成之后将继续执行。默认情况下,使用了同步上下文。如果不使用相同的同步上下文,则必须调用Task方法ConfigureAwait(continueOnCapturedContext:false)。看情况 因为切换线程的时候要同步上下文这样会比较耗时。
八:使用多个异步方法
在一个异步方法里,可以调用一个或多个异步方法。如何编写代码,取决于一个异步方法的结果是否依赖于另一个异步方法。
8.1:按顺序调用异步方法
;在同一个方法中,多个异步调用完全独立,互不影响,这样就按照顺序调用。
8.2:使用组合器
如果异步方法不依赖于其他异步方法,则每个异步方法都不适用await,而是把每个异步方法的返回结果复制给Task变量,就会运行得更快。GreetingAsync方法返回Task<string>。这些方法下i安在可以并行运行。组合器就可以帮助实现并行运行。
一个组合器可以接受多个同一类型的参数。并返回同一类型的值。
多个同一类型的参数被组合成一个参数来传递。Task组合器接受多个Task对象作为参数,并返回一个Task。
private static async void MultipleAsyncMethodsWithCombinatorsl() { Task<string> t1 = GreetingAsync("Stephanie"); Task<string> t2 = GreetingAsync("Matthias"); await Task.WhenAll(t1, t2); Console.WriteLine($"Finished both Method Result 1{t1.Result}: Result 2:{t2.Result}"); }
Task类定义了WhenAll和WhenAny组合器。从WhenAll方法返回的Task,是在所有传入方法的任务都完成了才会返回Task.从WhenAny方法返回的Task,是在其中一个传入方法的任务完成了就会返回Task。
Task.WhenAll可用域返回一个字符串数组,WhenAll有实现了多个重载。
private static async void MultipleAsyncMethodsWithCombinators2() { Task<string> t1 = GreetingAsync("Stephanie"); Task<string> t2 = GreetingAsync("Matthias"); string[] result=await Task.WhenAll(t1, t2); Console.WriteLine($"Finished both Method Result 1:{result[0]}: Result 2:{result[1]}"); }
九:转换异步模式
在.Net框架中有些类只提供了BeginXXX方法和EndXXX方法的异步模式,没有体哦那个基于任务的异步模式。
但是:可以把异步模式转换为基于任务的异步模式。
TaskFactory类定义了FromAsync方法,它可以把使用异步模式的方法转换成基于任务的异步模式的方法(TAP)。
获取当前线程的Id=Thread.CurrentThread.ManagedThreadId
private Func<string, string> greetingInvoker = Greeting; private IAsyncResult BeginGreeting(string name, AsyncCallback asyncCallback, object state) { return greetingInvoker.BeginInvoke(name, asyncCallback, state); } private string EndGreeting(IAsyncResult asyncResult) { return greetingInvoker.EndInvoke(asyncResult); } private async void ConvertingAsyncPattern() { string s = await Task<string>.Factory.FromAsync(BeginGreeting,EndGreeting, "Angela", null); Console.WriteLine(s); }
十:错误处理
注意点:返回void的异步方法不会等待,这是因为从async void方法抛出的异常无法捕获。因此异步方法最好返回一个Task类型,处理程序方法或重写的基类方法不受此规则限制。
1:异步方法的异常处理
使用await关键字,将其放在try/catch语句中,异步调用ThrowAfter方法后,HandleOneError方法就会释放线程,但它会在任务完成时保持任务的引用。
private static async void HandleOneError() { try { await ThrowAfter(200, "first"); } catch (Exception e) { Console.WriteLine(e.Message); } }
2:多个异步方法的异常处理
如果调用两个异步方法,每个都会抛出异常怎么处理?
并不是按照第一个异步方法处理完后抛出异常,第二个异步方法也抛出异常。
当第一个异步方法抛出异常后,就没有在继续调用第二个异步方法了。
private static async void StartTwoTasks() { try { await ThrowAfter(2000, "first"); await ThrowAfter(1000, "Second"); } catch (Exception e) { Console.WriteLine($"Handled:{e.Message}"); } }
并行调用这两个ThrowAfter方法。第一个ThrowAfter方法2s后抛出异常,1s后第二个ThrowAfter也抛出异常。使用Task.WhenAll,不管任务是否抛出异常,都会等到两个任务完成。
但是这个还是只能看见传递给WhenAll方法的第一个任务异常信息。没有先释第二个任务的异常信息,但该任务也在列表中。
private static async void StartTwoTasksParallel() { try { var t1= ThrowAfter(2000, "first"); var t2= ThrowAfter(1000, "Second"); await Task.WhenAll(t1, t2); } catch (Exception e) { Console.WriteLine($"Handled:{e.Message}"); } }
有一种方式可以获取所有任务的异常信息,就是在try块外声明任务变量t1和t2,使他们可以在catch块内访问。 这里可以使用IsFaulted属性检查任务的状态 ,以确认他们是否为出错状态。
若出现异常,IsFaulted属性会返回true.可以使用Task类的Exception.InnderException访问异常信息本身。另一种获取任务的异常信息更好的方式是下面十一的方法
十一:使用AggregateException信息(聚合异常信息)
为了获取所有任务失败的异常信息,可以将Task.WhenAll返回结果写道一个Task变量中。这个任务会一直等到所有任务都结束。否则仍然可能错过抛出的异常。
Exception属性是AggregateException类型的。这个异常类型定义了InnerExceptions属性,它包含了等待中的所有异常的列表。可以遍历所有异常。
private static async void ShowAggregateException() { Task taskResult = null; try { var t1 = ThrowAfter(2000, "first"); var t2 = ThrowAfter(1000, "Second"); await (taskResult= Task.WhenAll(t1, t2)); } catch (Exception e) { Console.WriteLine($"Handled:{e.Message}"); if (taskResult?.Exception.InnerExceptions != null) foreach (var exception in taskResult.Exception.InnerExceptions) { Console.WriteLine($"inner exception {exception.Message}"); } } }
十二:取消
在后台任务可以运行很长时间,取消任务就非常有用。
.Net提供了一种标准机制。这猴子那个机制可用于基于任务的异步模式。
取消框架基于协助的行为,不是强制性的。
一个运行时间很长的任务需要检查自己是否被取消,在这种情况下,它的工作就是清理所有已打开的资源,并结束相关工作。
取消基于CancellationTokenSource类。该类可以用域发送取消请求。
请求发送给引用CancellationToken类的任务,其中CancellationToken类与CancellationTokenSource类相关联。
12.1开始取消任务
1:定义一个私有成员字段CancellationTokenSource变量,该成员用于取消任务,并将令牌传递给应取消的方法。
private CancellationTokenSource _cts;
调用
_cts.Cancel();
CancellationTokenSource类还支持在指定时间后才取消任务。CancelAfter方法传入一个时间值,单位是毫秒,在该时间过后,就取消任务。
12.2 使用框架特性取消任务
HttpClient类的GetAsync方法可以接收CancellationToken参数。用于定期检查是否应取消操作。如果取消,就清理资源,之后抛出OperationCanceledException异常。
GetAsync(url,_cts.Token);
12.3 取消自定义任务
如何取消自定义任务?Task类Run方法提供了重载版本,它也传递CancellationToken参数。
但是,对于自定义任务,需要检查是否请求了取消操作。
可以使用IsCancellationRequsted属性检查令牌,在抛出异常之前,如果需要做一些清理工作,最好验证一下是否请求取消操作。如果不需要做清理工作。检查之后,会立即调用ThrowCancellationRequested方法触发异常。
await Task.Run(()=>{ //这边_cts 都是上面定义CancellationTokenSource的私有字段。
_cts.Token.ThrowCancellationRequested(); },_cts.Token)