异步编程、线程和任务

用鼠标操作,我们习惯了延迟,过去几十年都是这样。有了触摸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 }
View Code

  调度程序提供了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 }
View Code

基于任务的异步模式:

  使用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 }
View Code

  定义方法GreetingAsync(),可以使方法异步化。

  基于任务的异步模式指在异步方法名后加上Async作为后缀,并返回一个任务。

1 // Task<string>定义了一个返回字符串的任务
2 public Task<string> GreetingAsync()
3 {
4     return Task<string>.Run(() =>
5         {
6             return "hello";
7         });
8 }
View Code

然后调用异步方法

  使用await关键字来调用返回任务的异步方法GreetingAsync。使用await关键字需要有async修饰符声明的方法。

1 private async void CallerWithAsync()
2 {
3     string result = await GreetingAsync();
4     Console.WriteLine(result);
5 }
View Code

延续任务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 }
View Code

  如果异步方法不依赖于其它异步方法,就可以不使用await关键字调用单个异步方法了,而是使用await Task.WhenAll(t1,t2...)。这样运行的更快。

1 Task<string> t1 = GreetingAsync();
2 Task<string> t2 = GreetingAsync();
3 
4 string[] result = Task.WhenAll(t1,t2);
View Code

  如果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 }
View Code

多个异步方法的异常处理:

  如果每个异步方法采用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 }
View Code

  如果上面的异步方法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 }
View Code

  长时间任务需要自己检查自己是否被取消了。有两种情况:

  使用框架特性取消任务:

    框架中的某些异步方法通过提供可以传入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); 
View Code

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());
View Code

  同步执行任务:

    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类

原文地址:https://www.cnblogs.com/lztwj/p/4639677.html