用鼠标操作,我们习惯了延迟,过去几十年都是这样。有了触摸UI,应用程序要求立刻响应用户的请求。
C#5.0提供了更强大的异步编程,仅添加了两个新的关键字:async和await。
使用异步编程,方法调用是在后头运行(通常在线程和任务的帮助下),并且不会阻塞调用线程。
=》 所以 异步编程应该就是使用线程和任务进行编程。
另外,异步委托也是在线程和任务的帮助下完成的,它是基于事件的异步模式。
任务是对线程和线程池的进一步抽象。
1.首先先了解一个概念:(WPF的,应该和WindowsForm差不多)
在WindowsForm和WPF程序中,拥有同步上下文的线程其实就是UI线程。意思就是WPF元素(窗口中显示的WPF对象)具有线程关联性,创建WPF元素的线程拥有所创建的元素,其它线程不能直接与这些WPF元素进行交互。
拥有线程关联性的WPF对象(应该就是窗口中显示的WPF对象,又叫WPF可视化对象)在类层次的某个位置继承自DispatherObject类。DispatherObject类提供了核实代码是否在正确的线程上执行、并且(如果没有在正确的线程上)是否能切换到正确的线程上的能力。
Dispatcher类(调度程序):它继承自DispatherObject基类,任何WPF可视化对象也继承自DispatherObject基类。
通过 对象.Dispather来访问管理该对象的调度程序。
成员:
Dispatcher:返回管理该对象的调度程序。
CheckAccess(): 如果代码在正确的线程上使用对象,就返回true,否则返回false。
VerifyAccess(): 如果代码在正确的线程上使用对象,就什么也不做,否则抛出InvalidOperationException异常。
1 //Dispather调度程序的使用 2 //在按钮的单击事件中创建线程来更新UI 3 private void Button_Click(object sender, RoutedEventArgs e) 4 { 5 Thread thread = new Thread(UpdataTextWrong); 6 thread.Start(); 7 } 8 9 private void UpdataTextWrong() 10 { 11 txt.Text = "Here is some new text."; // error 12 //TextBox对象通过调用VerityAccess()方法捕获这一非法操作。 13 14 this.Dispather.BeginInvoke(DispatherPriority.Normal, 15 (ThreadStart) delegate() { 16 txt.Text = "Here is some new text."; 17 }); //Ok 18 }
调度程序提供了Invoke()和BeginInvoke()方法。
Invoke方法将指定的代码封送到调度程序线程(UI),Invoke方法会阻塞线程,直到调度程序执行了你指定的代码。如果需要暂停异步操作,直到用户通过UI提供一些反馈,可以使用Invoke()。
BeginInvoke方法提供了Invoke方法的异步模式,不会阻塞线程。
2.再介绍下什么是异步编程?
三种不同模式的异步编程:异步模式、基于事件的异步模式、基于任务的异步模式(TAP)。
异步模式:
有些类提供了同步方法,也提供了同步方法的异步方法版本,如BeginXXX和EndXXX模式的方法。
HttpWebRequest类提供了这种模式,提供了BeginGetResponse()和EndGetResponse()。
委托类型定义了Invoke方法用于调用同步方法,并且定义了一个BeginInvoke方法和一个EndInvoke方法,用来采用异步模式调用方法。
基于事件的异步模式:
基于事件的异步模式定义了一个带有“Async”后缀的方法。
例如,对于同步方法DownloadString,WebClient类提供了一个异步变体方法DownloadAsync。
更具代表的类:BackgroundWorker类实现了基于事件的异步方法。并提供进度报告和取消支持。
BackgroundWorker成员:
RunWorkerAsync()方法: 开始执行。
DoWork事件: 需要执行的耗时任务。当BackgroundWorker对象调用RunWorkerAsync()方法后,从CLR线程池中提取一个自由线程,并在自由线程上触发DoWork事件。因此它不能访问共享数据(如窗口类中的字段)或用户界面。
RunWorkerCompletedEventArgs事件: DoWork事件结束后触发,运行在调度程序(UI线程)上。
WorkerReportsProgress属性: 要为进度添加支持,就必须设为true。
ReportProgress()方法: 报告进度。
ProgressChanged事件: 调用ReportProgress()方法报告进度时触发。可响应该事件,读取新的进度百分比并更新用户界面。此事件从用户界面线程(调度线程)引发,无需Dispatcher。
WorkerSupportsCancellation属性: 需要添加取消支持,就必须设为true。
CancellationPending属性: 检查任务是否被取消。一般在DoWork事件中的循环操作中检查此属性。
CancelAsync()方法: 取消请求。调用此方法不会执行任何取消的行为,只是将CancellationPending属性设为true,表示用户已经取消了任务。
Cancel属性: 设置任务被取消了,以完成取消操作。
1 //BackgroundWorker组件的使用总结: 2 //1.在DoWork事件中处理耗时任务。 3 //2.如果需要进度报告,就将WorkerReportsProgress属性设置为true。在DoWork事件中调用ReportProgress()方法,ReportProgress()可以触发ProgressChanged事件,在ProgressChanged事 件中更新Ui。 4 //3.如果需要取消支持,就将WorkerSupportsCancellation属性设置为true,在其它如按钮事件下执行CancelAsync()方法执行取消。然后在DoWork事件中检查取消请求CancellationPending属性 。如果任务被取消了,就设置Cancel属性为true,以设置任务是被取消才结束的。 5 //4.在RunWorkerCompleted事件中做任务结束的响应处理。 6 //5.通过调用RunWorkerAsync()方法启动。 7 8 public BackgroundWorker backgroundWorker; 9 backgroundWorker.RunWorkerAsync()//启动 10 11 //DoWork事件 12 private void bcakgroundWorker_DoWork(object sender, DoWorkEventArgs e) 13 { 14 //更新进度 15 if(backgroundWorker.WorkerReportsProgress) 16 { 17 backgroundWorker.ReportProgress(50); 18 } 19 20 //检查取消 21 if(backgroundWorker.CancellationPending) 22 { 23 e.Cancel = true; // 设置为取消 24 return; 25 } 26 } 27 28 //RunWorkerCompleted事件 29 private void background_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) 30 { 31 // dosomething. 32 } 33 34 //ProgressChanged事件 35 backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) 36 { 37 //更新进度条之类的任务 38 } 39 40 //取消 41 private void DoCancel(object sender, RoutedEventArgs e) 42 { 43 backgroundWorker.CancelAsync(); 44 }
基于任务的异步模式:
使用await async关键字,没有阻塞,也不需要切换回UI线程,这些都是自动实现的。
代码在遇到await关键字后,会挂起当前线程,异步执行await的异步方法,不阻塞UI线程响应其它用户请求,当异步方法执行完毕后,再从挂起处继续执行。
挂起不阻塞,再从挂起处继续执行,这就是ContinueWith方法的作用。原理是编译器把await关键字后面的所有代码放进ContinueWith方法的代码块中来转换await关键字。
详细可以看下面的3。
3.异步编程的基础 (关键字 Task await async 异步方法) (基于任务的异步编程基础)
异步方法:异步执行,多线程执行的方法。await async修饰的是返回Task类型的异步方法。
有一个同步方法Greeting(),返回一个字符串。
1 public string Greeting() 2 { 3 return "hello"; 4 }
定义方法GreetingAsync(),可以使方法异步化。
基于任务的异步模式指在异步方法名后加上Async作为后缀,并返回一个任务。
1 // Task<string>定义了一个返回字符串的任务 2 public Task<string> GreetingAsync() 3 { 4 return Task<string>.Run(() => 5 { 6 return "hello"; 7 }); 8 }
然后调用异步方法
使用await关键字来调用返回任务的异步方法GreetingAsync。使用await关键字需要有async修饰符声明的方法。
1 private async void CallerWithAsync() 2 { 3 string result = await GreetingAsync(); 4 Console.WriteLine(result); 5 }
延续任务ContinueWith:
语法: Task对象.ContinueWith(后续执行的任务);
按顺序调用异步方法:
//t2会等待t1完成后才进行,当t2依赖于t1时,这是个好方法。
1 private async void MultipleAsyncMethods() 2 { 3 string s1 = await GreetingAsync(); //t1 4 string s2 = await GreetingAsync(); //t2 5 }
如果异步方法不依赖于其它异步方法,就可以不使用await关键字调用单个异步方法了,而是使用await Task.WhenAll(t1,t2...)。这样运行的更快。
1 Task<string> t1 = GreetingAsync(); 2 Task<string> t2 = GreetingAsync(); 3 4 string[] result = Task.WhenAll(t1,t2);
如果t1,t2返回类型相同,可以使用数组来接受返回结果。
如果t1,t2返回类型不同,可以使用t1.Result、t2.Result来返检查返回结果。
转换异步模式:
并非所有类在.Net4.5中引入了基于任务的异步方法,还是有很多类只提供了BeginXXX和EndXXX的方法。但是可以用TaskFactory.FromAsync()方法把使用异步模式的方法转换为基于任务的异步模式的方法(TAP)。
Task.FormAsync()的使用:不会
基于任务的异步方法的异常处理:
异步方法异常的一个较好的处理方式,就是使用await关键字,然后将其放在try/catch语句中。
1 try 2 { 3 await XXX(); 4 } 5 catch(Exception e) 6 { 7 //处理 8 }
多个异步方法的异常处理:
如果每个异步方法采用await关键字,顺序执行时,前面的异步方法异常了,后面的异步方法就不执行了。
如果采用并行运行,即使用Task.WhenAll,Task.WhenAny时,看如下示例代码:
1 try 2 { 3 Task t1 = XXX(); 4 Task t2 = XXX(); 5 6 await Task.WhenAll(t1,t2); 7 } 8 Catch(Exception ex) 9 { 10 11 }
如果上面的异步方法XXX()会抛出异常,那么两个异步方法都会执行,但是在catch块中只能看到第一个异步方法的异常信息。
想要看到所有异步方法的异常信息,可以将t1,t2声明在try块外面,这样在catch块中就能访问到t1,t2的异常信息了。
取消:
后台任务可能运行很长时间,取消任务就很有必要了。
取消框架基于协助行为,不是强制性的。一个运行时间很长的任务需要检查自己是否被取消。
取消基于CancellationTokenSource类,该类用于发送取消请求。取消被发送给了引用CancellationToken类的任务。
看示例代码:
1 //定义一个CancellationTokenSource对象 2 private CancellationTokenSource cts; 3 4 //添加取消按钮,在这个方法中,变量cts用Cancel()方法取消任务。 5 private void OnCancel(object sender, RoutedEventArgs e) 6 { 7 if(cts != null) 8 { 9 cts.Cancel(); //发送取消请求,请求被发送给引用了Cancellation类的任务。任务需要自己检查自己是否被取消了。 10 } 11 }
长时间任务需要自己检查自己是否被取消了。有两种情况:
使用框架特性取消任务:
框架中的某些异步方法通过提供可以传入CancellationToken的重载来支持取消任务。
如HttpClient类的GetAsync方法提供了传入的CancellationToken参数,GetAsync方法的实现会定期检查是否应取消操作。如果取消,就清理资源,抛出异常。
取消自定义任务:
Task的Run方法提供了重载版本,可以传入CancellationToken参数。
使用IsCancellationRequested属性检查是否已经取消,如果取消可以调用ThrowIfCancellationRequested方法引发异常。
1 await Task.Run(() => 2 { 3 If(IsCancellationRequested) //检查 4 { 5 cts.Token.ThrowIfCancellationRequested();//引发异常 6 } 7 },cto.Token);
4.任务
在.Net4.0之前,必须直接使用Thread和ThreadPool类编写程序,现在.Net对这两个类做了抽象,允许使用Parallel和Task。
4.1 Parallel类
提供了数据和任务并行性(并行和串行、同步和异步不要搞混)。总结为:多线程并发、非异步、会阻塞。
Parallel.For()和Parallel.ForEach()方法在每次迭代中调用相同的方法,用于数据并行。
Parallel.Invoke()方法允许调用不同的方法,用于任务并行。 Parallel.Invoke(Action,Action...);
4.2 任务
创建任务的方式:
1 var tf = new TaskFactory(); 2 Task t1 = tf.StartNew(TaskMethod); 3 4 Task t2 = Task.Factory.StartNew(TaskMethod); 5 6 Task t3 = new Task(TaskMethod); 7 8 Task t4 = Task.Run(() => TaskMethod());
同步执行任务:
t1.FunSynchronously();
异步执行任务(使用单独线程的任务):
t1.Start();
任务的结果:
t1.Result中。
4.3 任务的取消
已经在上面的3中说明了,使用的是CancellationTokenSource类。
适用于Parallel、Task中。
5.线程池和线程Thread
创建线程需要时间,所以系统事先创建好了很多线程,这就是线程池,有ThreadPool类托管。
ThreadPool.QueueUserWorkItem()可以使用线程池。
如果需要更多控制可以使用Thread类,该类允许创建前台线程,设置优先级。
6线程问题
争用条件:
如果两个或多个线程访问相同的对象,并且对共享状态的访问没有同步,就会出现争用条件。
死锁:
至少有两个线程被挂起,并等待对方解除锁定。
同步:
要避免同步问题,最好就不要在线程间共享数据,但这是不合理的。
如果需要共享数据,就必须使用同步技术,确保一次只有一个线程访问和改变共享状态。
可用于多线程的同步技术:
lock语句
Interlocked类
Monitor类
SpinLock结构
WaitHandle类
Mutex类
Semaphore类
Event类
Barrier类
ReaderWriterLockSlim类