• 异步编程、线程和任务


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

  • 相关阅读:
    C语言和go语言之间的交互
    Elasticsearch笔记九之优化
    Elasticsearch笔记八之脑裂
    Elasticsearch笔记七之setting,mapping,分片查询方式
    Elasticsearch笔记六之中文分词器及自定义分词器
    Elasticsearch笔记四之配置参数与核心概念
    Elasticsearch笔记三之版本控制和插件
    Elasticsearch笔记二之Curl工具基本操作
    Elasticsearch笔记五之java操作es
    Python处理Excel表格
  • 原文地址:https://www.cnblogs.com/lztwj/p/4639677.html
Copyright © 2020-2023  润新知