9159金沙游艺场

图片 3
教你用 HTML5 制作Flappy Bird(下)
图片 5
如何写shell脚本?尝试自己编写一个简单脚本

C#异步编程

目录

复习:

 IO操作的MDA(Direct memory
access)模式:直接访问内存,是一种不经过CPU而直接进行内存数据存储的数据交换模式,几乎可以不损耗CPU的资源;
 CLR所提供的异步编程模型就是充分利用硬件的DMA功能来释放CPU的压力;使用线程池进行管理,异步将工作移交给线程池中的某个工作线程来完成,直到异步完成,异步才会通过回调的方式通知线程池,让CLR响应异步完毕;

  • 1.1
    简介
  • 1.2
    创建任务
  • 1.3
    使用任务执行基本的操作
  • 1.4
    组合任务
  • 1.5
    将APM模式转换为任务
  • 1.6
    将EAP模式转换为任务
  • 1.7
    实现取消选项
  • 1.8
    处理任务中的异常
  • 1.9
    并行运行任务
  • 1.10
    使用TaskScheduler配置任务执行
  • 参考书籍
  • 笔者水平有限,如果错误欢迎各位批评指正!

第三章内容中我们提到了三种异步编程模型,这里简单复习一下,分别如下

它是并发的一种形式,它采用 future
模式或回调(callback)机制,以避免产生不必要的线程。一个 future(或
promise)类型代表一些即将完成的操作。在 .NET 中,新版 future 类型有Task
和Task<TResult>。 


1.APM(异步编程模式):形如Beginxxx,Endxxx。

异步编程模式——利用委托和线程池实现的模式

APM 异步编程模型,Asynchronous Programming Model            C#1.0

EAP 基于事件的异步编程模式,Event-based Asynchronous Pattern  C#2.0

TAP 基于任务的异步编程模式,Task-based Asynchronous Pattern    C#4.0

Asyncawait简化异步编程;任务并行库,Task Parallel Library     C#5

本系列首页链接:[C#多线程编程系列(一)-
简介 ]

2.EAP(基于事件的异步编程模式):这个我们在.net中使用到了BackgroudWorker组件,使用方法是通过事件绑定处理的方式。

APM

         使用IAsyncResult设计模式的异步操作是通过名为 BeginXXX 和 EndXXX
的两个方法来实现,这两个方法分别指开始和结束异步操作。该模式允许用更少的CPU资源(线程)去做更多的操作,.NET
Framework很多类也实现了该模式,同时我们也可以自定义类来实现该模式(也就是在自定义的类中实现返回类型为IAsyncResult接口的BeginXXX方法和接受IAsyncResult兼容类型作为唯一参数的EndXXX方法),另外委托类型也定义了BeginInvoke和EndInvoke方法。例如,FileStream类提供BeginRead和EndRead方法来从文件异步读取字节。这两个方法实现了
Read 方法的异步版本。

调用 BeginXXX
后,应用程序可以继续在调用线程上执行指令,同时异步操作在另一个线程上执行(如果有返回值还应调用
EndXXX结束异步操作,并向该方法传递BeginXXX
方法返回的IAsyncResult对象,获取操作的返回值)。

 图片 1

CompletedSynchronously属性值侧重与提示信息,而非操作

访问异步操作的结果,APM提供了四种方式:

1.在调用BeginXXX方法的线程上调用EndXXX方法来得到异步操作的结果;但是这种方式会阻塞调用线程,在知道操作完成之后调用线程才能继续运行。

2.循环查询IAsyncResult的IsComplete属性,操作完成后再调用EndXXX方法来获得操作返回的结果。

3.IAsyncResult的AsyncWaitHandle属性实现更加灵活的等待逻辑,调用该属性WaitOne()方法来使一个线程阻塞并等待操作完成;再调用EndXXX方法来获得操作的结果。WaitHandle.WaitOne()可以指定最长的等待时间,如超时返回false;

4.
在调用BeginXXX方法时提供AsyncCallback委托的实例作为参数,在异步操作完成后委托会自动调用(AsyncCallback对象)指定的方法。(首选方式)AsyncCallback委托仅能够调用符合特定模式的方法(只有一个参数IAsyncResult,且没有返回值);

图片 2图片 3

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Runtime.Remoting.Messaging;

namespace AsyncCallbackDelegate
{
    public delegate int BinaryOp(int x, int y);
    class Program
    {
        private static bool isDone = false;
        static void Main(string[] args)
        {
            Console.WriteLine("*****  AsyncCallbackDelegate Example *****");
            Console.WriteLine("Main() invoked on thread {0}.",
              Thread.CurrentThread.ManagedThreadId);
            BinaryOp b = new BinaryOp(Add);
            IAsyncResult iftAR = b.BeginInvoke(10, 10,
              new AsyncCallback(AddComplete),
              "Main() thanks you for adding these numbers.");//传入数据
            // Assume other work is performed here...
            while (!isDone)
            {
                Thread.Sleep(1000);
                Console.WriteLine("Working....");
            }
            Console.ReadLine();
        }

        #region Target for AsyncCallback delegate
        // Don't forget to add a 'using' directive for 
        // System.Runtime.Remoting.Messaging!
        static void AddComplete(IAsyncResult itfAR)
        {
            Console.WriteLine("AddComplete() invoked on thread {0}.",
              Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("Your addition is complete");

            // Now get the result.
            //AsyncCallback委托的目标无法调用其他方法中创建的委托
            //IAsyncResult itfAR 实际上是System.Runtime.Remoting.Messaging命名空间AsyncResult类的一个实例
            AsyncResult ar = (AsyncResult)itfAR;
            //AsyncDelegate静态属性返回原始异步委托引用
            BinaryOp b = (BinaryOp)ar.AsyncDelegate;
            Console.WriteLine("10 + 10 is {0}.", b.EndInvoke(itfAR));

            // Retrieve the informational object and cast it to string.
            //AsyncState属性获取 BeginInvoke第四个参数传入的值
            string msg = (string)itfAR.AsyncState;
            Console.WriteLine(msg);
            isDone = true;
        }

        #endregion

        #region Target for BinaryOp delegate
        static int Add(int x, int y)
        {
            Console.WriteLine("Add() invoked on thread {0}.",
              Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(5000);
            return x + y;
        }
        #endregion
    }
}

AsyncCallback

异常捕获

在同步执行的方法里面通常处理异常的方式是将可能抛出异常的代码放到try…catch…finally里面,之所以能够捕获到,是因为发生异常的代码与调用的代码位于同一个线程。当调用一个异步方法发生异常时,CLR会捕获并且在EndXXX方法时再次将异常抛出抛出,所以异步调用中的异常在EndXXX方法出捕获就行了。

class ApmExceptionHandling 
{
   public static void Go() 
  {
      WebRequest webRequest = WebRequest.Create("http://0.0.0.0/");
      webRequest.BeginGetResponse(ProcessWebResponse, webRequest);
      Console.ReadLine();
   }
   private static void ProcessWebResponse(IAsyncResult result) {
      WebRequest webRequest = (WebRequest)result.AsyncState;

      WebResponse webResponse = null;
      try {
         webResponse = webRequest.EndGetResponse(result);
         Console.WriteLine("Content length: " + webResponse.ContentLength);
      }
      catch (WebException we) {
         Console.WriteLine(we.GetType() + ": " + we.Message);
      }
      finally {
         if (webResponse != null) webResponse.Close();
      }
   }
}

APM WinForm UI线程回调

由于AsyncCallback委托回调是从ThreadPool中的线程执行的,因此对于Winform,如果回调需要操作UI控件,就需要返回到UI线程去,常用的两个方法:

1. 
Control类实现了ISynchronizeInvoke接口,提供了Invoke和BeginInvoke方法来支持其它线程更新GUI界面控件的机制(将回调方法投递到创建该控件的线程中执行)。

 图片 4

Control类的 Invoke,BeginInvoke 内部实现如下:

a)
Invoke(同步调用)先判断控件创建线程与当前线程是否相同,相同则直接调用委托方法;否则使用Win32API的PostMessage异步执行,但是Invoke内部会调用IAsyncResult.AsyncWaitHandle等待执行完成。

b) BeginInvoke(异步调用)使用Win32API的PostMessage 异步执行,并且返回
IAsyncResult 对象。

使用方式:回调方法中对控件检测InvokeRequired值,if
true,在该回调中封送一次委托,调用控件的Invoke/ BeginInvoke方法;

 图片 5

2.GUI(WinForm/WPF)应用程序引入了一个线程处理模型:创建窗口的线程是唯一能对那个窗口进行更新的线程;在GUI线程中,经常需要生成异步操作,使GUI线程不阻塞并停止响应用户输入。然而,异步操作完成时,由于是用一个线程池线程完成的,而线程池线程不能更新UI控件。为解决这些问题,FCL定义一个System.Threading.SynchronizationContext(线程同步上下文)的基类,其派生对象负责将一个应用程序模型连接到它的线程处理模型。

GUI线程都有一个和它关联的SynchronizationContext派生对象,使用其静态Current属性获取:SynchronizationContext
sc = SynchronizationContext.Current;
将此对象传给其他线程,当一个线程池线程需要让GUI线程更新UI时,调用该对象的sc.Post方法,向Post传递一个匹配SendOrPostCallback委托签名的回调方法(一般是更新UI的操作方法,由GUI线程去执行),以及一个要传给回调方法的实参。

SynchronizationContext
的Post方法和Send方法的区别:(分别对应于异步/同步调用)

Post方法将回调方法送人GUI线程的队列,允许程序池线程立即返回,不进行阻塞;Post方法内部调用了BeginInvoke方法;

Send方法也将回调方法送人GUI线程的队列,但随后就会阻塞线程池线程,直到GUI线程完成对回调方法的调用。阻塞线程池线程极有可能造成线程池创建一个新的线程,避免调用该方法;Send方法内部调用了Invoke方法; 

对winform来说是
System.Windows.Forms.WindowsFormsSynchronizationContext是其子类.

Winform窗口出现后,UI线程
SynchronizationContext.Current会被绑定赋值,只有UI线程的Current不为null。

Public class SendOrPostUI {
   public static void Go() {
      System.Windows.Forms.Application.Run(new MyWindowsForm());
   }
   private static AsyncCallback SyncContextCallback(AsyncCallback callback) {
      // Capture the calling thread's SynchronizationContext-derived object
      SynchronizationContext sc = SynchronizationContext.Current;
      // If there is no SC, just return what was passed in
      if (sc == null) return callback;
      // Return a delegate that, when invoked, posts to the captured SC a method that 
      // calls the original AsyncCallback passing it the IAsyncResult argument
      return asyncResult => sc.Post(result => callback((IAsyncResult)result), asyncResult);
   }
   private sealed class MyWindowsForm : System.Windows.Forms.Form {
      public MyWindowsForm() {
         Text = "Click in the window to start a Web request";
         Width = 400; Height = 100;
      }
      protected override void OnMouseClick(System.Windows.Forms.MouseEventArgs e) {
         // The GUI thread initiates the asynchronous Web request 
         Text = "Web request initiated";
         var webRequest = WebRequest.Create("http://Wintellect.com/");
         webRequest.BeginGetResponse(SyncContextCallback(ProcessWebResponse), webRequest);
         base.OnMouseClick(e);
      }
      private void ProcessWebResponse(IAsyncResult result) {
         // If we get here, this must be the GUI thread, it's OK to update the UI
         var webRequest = (WebRequest)result.AsyncState;
         using (var webResponse = webRequest.EndGetResponse(result)) {
            Text = "Content length: " + webResponse.ContentLength;
         }
      }
   }
}

比较两种方法其实差不太多,一个是回调内再次包装,一个是包装原来的回调。但是SynchronizationContext业务层与UI分离来讲的话是比较好;


3.TPL(基于任务的异步编程模式):这个就会用到任务并行库。

EAP

EAP是为了更便于处理UI的更新推出的模式,主要优点:它同Visual Studio UI设计器进行了很好的集成,可将大多数实现了EAP的类拖放到设计平面(design surface)上,双击控件对应的XXXCompleted事件名,会自动生成事件的回调方法,并将方法同事件自身联系起来。EAP保证事件在应用程序的GUI线程上引发,允许事件回调方法中的代码更新UI控件;

EAP另一重要功能:支持EAP的类自动将应用程序模型映射到它的线程处理模型;EAP类在内部使用SynchronizationContext类。有的EAP类提供了取消、进度报告功能。

   FCL中只有17个类型实现了EAP模式,一般有一个
XXXAsync方法和一个对应的XXXCompleted事件,以及这些方法的同步版本:

*       System.Object的派生类型:*

*                  System.Activies.WorkflowInvoke  *

*                 
System.Deployment.Application.ApplicationDeployment*

*                  System.Deployment.Application.InPlaceHosingManager*

*                  System.Net.Mail.SmtpClient*

*                  System.Net.PeerToPeer.PeerNameResolver*

*                  System.Net.PeerToPeer.Collaboration.ContactManager*

*                  System.Net.PeerToPeer.Collaboration.Peer*

*                  System.Net.PeerToPeer.Collaboration.PeerContact*

*                  System.Net.PeerToPeer.Collaboration.PeerNearMe*

*                 
System.ServiceModel.Activities.WorkflowControlClient*

*                  System.ServiceModel.Discovery.AnnoucementClient*

*                  System.ServiceModel.Discovery.DiscoveryClient*

*      System.ComponentModel.Component的派生类型:*

                 
System.ComponentModel.BackgroundWorker

                 
System.Media.SoundPlay

                 
System.Net.WebClient

                 
System.Net.NetworkInformation.Ping

                 
System.Windows.Forms.PictureBox(继承于Control类,Control类派生于Component类)

private sealed class MyForm : System.Windows.Forms.Form {
    protected override void OnClick(EventArgs e) {
      // The System.Net.WebClient class supports the Event-based Asynchronous Pattern
      WebClient wc = new WebClient();
      // When a string completes downloading, the WebClient object raises the 
      // DownloadStringCompleted event which will invoke our ProcessString method         
      wc.DownloadStringCompleted += ProcessString;
      // Start the asynchronous operation (this is like calling a BeginXxx method)
      wc.DownloadStringAsync(new Uri("http://Wintellect.com"));
      base.OnClick(e);
    }
    // This method is guaranteed to be called via the GUI thread
    private void ProcessString(Object sender, DownloadStringCompletedEventArgs e) {
      // If an error occurred, display it; else display the downloaded string
      System.Windows.Forms.MessageBox.Show((e.Error != null) ? e.Error.Message : e.Result);
      }
   }

BackgroundWorker:只有该类型用于可用于执行异步的计算限制的工作;提供三个事件:

DoWork:向这个事件登记的方法应该包含计算限制的代码。这个事件由一个线程池线程调用RunWorkerAsync(两个重载方法,带参的方法是向DoWork登记的方法的DoWorkEventArgs参数对象的Argument属性传值,只能在登记的方法中(如e.Argument)获取,Result属性必须设置成计算限制的操作希望返回的值)时引发;

ProgressChanged:向这个事件登记的方法应该包含使用进度信息来更新UI的代码。这个事件总是在GUI线程上引发。DoWork登记的方法必须定期调用BackgroundWorker的ReportProgress方法来引发ProgressChanged事件;

RunWorkerCompleted:向这个事件登记的方法应该包含使用计算限制操作的结果对UI进行更新的代码。这个事件总是在GUI线程上引发。Result获取表示异步操作的结果;

公共属性:CancellationPending(标识是否已请求取消后台操作)、IsBusy(标识是否正在运行异步操作)、WorkReportsProgress(获取/设置是否报告进度更新)、WorkerSupportsCancellation(获取/设置是否支持异步取消)

公共方法:CancelAsync(请求取消挂起的后台操作)、ReportProgress、RunWorkerAsync

异常

异常不会抛出。在XXXCompleted事件处理方法中,必须查询AsyncCompletedEventArgs的Exception属性,看它是不是null。如果不是null,就必须使用if语句判断Exception派生对象的类型,而不是使用catch块。

1.1 简介

在之前的几个章节中,就线程的使用和多线程相关的内容进行了介绍。因为线程涉及到异步、同步、异常传递等问题,所以在项目中使用多线程的代价是比较高昂的,需要编写大量的代码来达到正确性和健壮性。

为了解决这样一些的问题,在.Net Framework 4.0中引入了一个关于一步操作的API。它叫做任务并行库(Task
Parallel
Library)
。然后在.Net Framwork 4.5中对它进行了轻微的改进,本文的案例都是用最新版本的TPL库,而且我们还可以使用C#
5.0的新特性await/async来简化TAP编程,当然这是之后才介绍的。

TPL内部使用了线程池,但是效率更高。在把线程归还回线程池之前,它会在同一线程中顺序执行多少Task,这样避免了一些小任务上下文切换浪费时间片的问题。

任务是对象,其中封装了以异步方式执行的工作,但是委托也是封装了代码的对象。任务和委托的区别在于,委托是同步的,而任务是异步的。

在本章中,我们将会讨论如何使用TPL库来进行任务之间的组合同步,如何将遗留的APM和EAP模式转换为TPL模式等等。

 

TAP

.NET4.0
中引入了新的异步编程模型“基于任务的异步编程模型(TAP)”,并且推荐我们在开发新的多线程应用程序中首选TAP,在.NET4.5中更是对TPL库进行了大量的优化与改进(async和await)。那现在我先介绍下TAP具有哪些优势:

  1. 任务调度器(TaskScheduler)依赖于底层的线程池引擎,可自定义一个TaskScheduler更改调度算法,同时不更改代码或编程模型。通过局部队列的任务内联化(task
    inlining)和工作窃取(work-stealing)机制而发起了大量任务,Task可以为我们提升程序性能。
  2. 可以使用PreferFairness标志,获取与ThreadPool.QueueUserWorkItem或者一个委托的BeginInvoke相同的线程池行为。

        3.
 轻松实现任务等待、任务取消、延续任务、异常处理(System.AggregateException)、GUI线程操作。

       4.  在任务启动后,可以随时以任务延续的形式注册回调。

       5.  充分利用现有的线程,避免创建不必要的额外线程。

       6.  结合C#5.0引入async和await关键字轻松实现“异步方法”。

APM转换为TAP:

使用TaskFactory的FromAsync方法,传递四个实参:BeginXxx方法、EndXxx方法、Object状态、可选的TaskCreationOptions值,返回对一个Task对象的引用;

private static void ConvertingApmToTask() {
      // Instead of this:
      WebRequest webRequest = WebRequest.Create("http://Wintellect.com/");
      webRequest.BeginGetResponse(result => {
         WebResponse webResponse = null;
         try {
            webResponse = webRequest.EndGetResponse(result);
            Console.WriteLine("Content length: " + webResponse.ContentLength);
         }
         catch (WebException we) {
            Console.WriteLine("Failed: " + we.GetBaseException().Message);
         }
         finally {
            if (webResponse != null) webResponse.Close();
         }
      }, null);
      Console.ReadLine();  // for testing purposes
      // Make a Task from an async operation that FromAsync starts
      webRequest = WebRequest.Create("http://Wintellect.com/");
      var t1 = Task.Factory.FromAsync<WebResponse>(webRequest.BeginGetResponse, webRequest.EndGetResponse, null, TaskCreationOptions.None);
      var t2 = t1.ContinueWith(task => {
         WebResponse webResponse = null;
         try {
            webResponse = task.Result;
            Console.WriteLine("Content length: " + webResponse.ContentLength);
         }
         catch (AggregateException ae) {
            if (ae.GetBaseException() is WebException)
               Console.WriteLine("Failed: " + ae.GetBaseException().Message);
            else throw;
         }
         finally { if (webResponse != null) webResponse.Close(); }
      });
      try {t2.Wait();  // for testing purposes only}
      catch (AggregateException) { }
   }

EAP转换成TAP:

使用System.Threading.Tasks.TaskCompletionSource类进行包装;

图片 6

当构造一个TaskCompletionSource对象,也会生成一个Task,可通过其Task属性获取;当一个异步操作完成时,它使用TaskCompletionSource对象来设置它因为什么而完成,取消,未处理的异常或者它的结果。调用某个SetXxx方法,可以设置底层Task对象的状态。

private sealed class MyFormTask : System.Windows.Forms.Form {
      protected override void OnClick(EventArgs e) {
         // The System.Net.WebClient class supports the Event-based Asynchronous Pattern
         WebClient wc = new WebClient();
         // Create the TaskCompletionSource and its underlying Task object
         var tcs = new TaskCompletionSource<String>();
         // When a string completes downloading, the WebClient object raises the 
         // DownloadStringCompleted event which will invoke our ProcessString method
         wc.DownloadStringCompleted += (sender, ea) => {
            // This code always executes on the GUI thread; set the Task’s state
            if (ea.Cancelled) tcs.SetCanceled();
            else if (ea.Error != null) tcs.SetException(ea.Error);
            else tcs.SetResult(ea.Result);
         };
         // Have the Task continue with this Task that shows the result in a message box
// NOTE: The TaskContinuationOptions.ExecuteSynchronously flag is required to have this code
         // run on the GUI thread; without the flag, the code runs on a thread pool thread 
         tcs.Task.ContinueWith(t => {
            try { System.Windows.Forms.MessageBox.Show(t.Result);}
            catch (AggregateException ae) {
               System.Windows.Forms.MessageBox.Show(ae.GetBaseException().Message);
            }
         }, TaskContinuationOptions.ExecuteSynchronously);
         // Start the asynchronous operation (this is like calling a BeginXxx method)
         wc.DownloadStringAsync(new Uri("http://Wintellect.com"));
         base.OnClick(e);
      }
   }

实现了TAP的类:存在XxxTaskAsync的方法,
支持异步操作的取消和进度的报告的功能;

取消:可以通过协作式取消模式,向异步方法传入CancellationToken 参数,通过调用其ThrowIfCancellationRequested方法来定时检查操作是否已经取消;

进度报告:可以通过IProgress<T>接口来实现进度报告的功能;

更新GUI:
TaskScheduler.FromCurrentSynchronizationContext()获取同步上下文任务调度器,将关联该对象的所有任务都调度给GUI线程,使任务代码能成功更新UI;

private sealed class MyForm : System.Windows.Forms.Form {
        public MyForm() {
            Text = "Synchronization Context Task Scheduler Demo";
            Visible = true; Width = 400; Height = 100;
        }
         private static Int32 Sum(CancellationToken ct, Int32 n) {
        Int32 sum = 0;
        for (; n > 0; n--) {
            // The following line throws OperationCanceledException when Cancel 
            // is called on the CancellationTokenSource referred to by the token
            ct.ThrowIfCancellationRequested();
            //Thread.Sleep(0);   // Simulate taking a long time
            checked { sum += n; }
        }
        return sum;
       }
        private readonly TaskScheduler m_syncContextTaskScheduler =
           TaskScheduler.FromCurrentSynchronizationContext();
        private CancellationTokenSource m_cts;
        protected override void OnMouseClick(System.Windows.Forms.MouseEventArgs e) {
            if (m_cts != null) {    // An operation is in flight, cancel it
                m_cts.Cancel();
                m_cts = null;
            } else {                // An operation is not in flight, start it
                Text = "Operation running";
                m_cts = new CancellationTokenSource();
           // This task uses the default task scheduler and executes on a thread pool thread
                var t = new Task<Int32>(() => Sum(m_cts.Token, 20000), m_cts.Token);
                t.Start();
 // These tasks use the synchronization context task scheduler and execute on the GUI thread
                t.ContinueWith(task => Text = "Result: " + task.Result,
                   CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion,
                   m_syncContextTaskScheduler);
                t.ContinueWith(task => Text = "Operation canceled",
                   CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled,
                   m_syncContextTaskScheduler);
                t.ContinueWith(task => Text = "Operation faulted",
                   CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted,
                   m_syncContextTaskScheduler);
            }
            base.OnMouseClick(e);
        }
}

异常处理

在任务抛出的未处理异常都封装在System.AggregateException对象中。这个对象会存储在方法返回的Task或Task<TResult>对象中,需要通过访问Wait()、Result、Exception成员才能观察到异常。(所以,在访问Result之前,应先观察IsCanceled和IsFaulted属性)

假如一直不访问Task的Wait()、Result、Exception成员,那么你将永远注意不到这些异常的发生。为了帮助你检测到这些未处理的异常,可以向TaskScheduler对象的UnobservedTaskException事件注册回调函数。每当一个Task被垃圾回收时,如果存在一个没有注意到的异常,CLR的终结器线程会引发这个事件。

可在事件回调函数中调用UnobservedTaskExceptionEventArgs对象的SetObserved()
方法来指出已经处理好了异常,从而阻止CLR终止线程。然而并不推荐这么做,宁愿终止进程也不要带着已经损坏的状态继续运行。

1.2 创建任务

在本节中,主要是演示了如何创建一个任务。其主要用到了System.Threading.Tasks命名空间下的Task类。该类可以被实例化并且提供了一组静态方法,可以方便快捷的创建任务。

在下面实例代码中,分别延时了三种常见的任务创建方式,并且创建任务是可以指定任务创建的选项,从而达到最优的创建方式。

TaskCreationOptions中一共有7个枚举,枚举是可以使用|运算符组合定义的。其枚举如下表所示。

成员名称 说明
AttachedToParent 指定将任务附加到任务层次结构中的某个父级。 默认情况下,子任务(即由外部任务创建的内部任务)将独立于其父任务执行。 可以使用 TaskContinuationOptions.AttachedToParent 选项以便将父任务和子任务同步。请注意,如果使用 DenyChildAttach 选项配置父任务,则子任务中的 AttachedToParent 选项不起作用,并且子任务将作为分离的子任务执行。有关详细信息,请参阅附加和分离的子任务
DenyChildAttach 指定任何尝试作为附加的子任务执行(即,使用 AttachedToParent 选项创建)的子任务都无法附加到父任务,会改成作为分离的子任务执行。 有关详细信息,请参阅附加和分离的子任务
HideScheduler 防止环境计划程序被视为已创建任务的当前计划程序。 这意味着像 StartNew 或 ContinueWith 创建任务的执行操作将被视为 Default 当前计划程序。
LongRunning 指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件。 它会向 TaskScheduler 提示,过度订阅可能是合理的。 可以通过过度订阅创建比可用硬件线程数更多的线程。 它还将提示任务计划程序:该任务需要附加线程,以使任务不阻塞本地线程池队列中其他线程或工作项的向前推动。
None 指定应使用默认行为。
PreferFairness 提示 TaskScheduler 以一种尽可能公平的方式安排任务,这意味着较早安排的任务将更可能较早运行,而较晚安排运行的任务将更可能较晚运行。
RunContinuationsAsynchronously 强制异步执行添加到当前任务的延续任务。请注意,RunContinuationsAsynchronously 成员在以 .NET Framework 4.6 开头的 TaskCreationOptions 枚举中可用。
static void Main(string[] args)
{
    // 使用构造方法创建任务
    var t1 = new Task(() => TaskMethod("Task 1"));
    var t2 = new Task(() => TaskMethod("Task 2"));

    // 需要手动启动
    t2.Start();
    t1.Start();

    // 使用Task.Run 方法启动任务  不需要手动启动
    Task.Run(() => TaskMethod("Task 3"));

    // 使用 Task.Factory.StartNew方法 启动任务 实际上就是Task.Run
    Task.Factory.StartNew(() => TaskMethod("Task 4"));

    // 在StartNew的基础上 添加 TaskCreationOptions.LongRunning 告诉 Factory该任务需要长时间运行
    // 那么它就会可能会创建一个 非线程池线程来执行任务  
    Task.Factory.StartNew(() => TaskMethod("Task 5"), TaskCreationOptions.LongRunning);

    ReadLine();
}

static void TaskMethod(string name)
{
    WriteLine($"任务 {name} 运行,线程 id {CurrentThread.ManagedThreadId}. 是否为线程池线程: {CurrentThread.IsThreadPoolThread}.");
}

运行结果如下图所示。

图片 7

4.1 简介

Async /Await

在.NET Framework 4.0中添加.NET Framework
4.5中新的异步操作库(async/await),该包由三个库组成:Microsoft.Bcl、Microsoft.Bcl.Async和Microsoft.Bcl.Build。

Install-Package Microsoft.Bcl.Async

注:asp.net
框架必须要升级.net framework框架才能使用 async/await

如果异常信息是“Message : Could not load file or assembly ‘System.Core,
Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e,
Retargetable=Yes’ or one of its dependencies. The given assembly name or
codebase was invalid. (Exception from HRESULT: 0x80131047)”,

那需要你去微软官网下载.net4.0的KB2468871补丁来安装。

C# 5引入了异步函数(asynchrnous
function)的概念。通常是指用async修饰符声明的,可

包含await表达式的方法或匿名函数;

async关键字创建了一个状态机,类似于yield
return语句;await关键字只能用于有用async修饰符声明的方法。async修饰符只能用于返回Task/Task<TResult>或void的方法。await只能用来调用返回Task/Task<TResult>的方法;await会解除线程的阻塞,完成调用的任务;等待任务完成后,获取结果,然后执行await关键字后面的代码;编译器会把await的表达式后的代码使用
Task.ContinueWith
包装了起来,回调时默认使用当前线程的同步上下文任务调度器;如果不使用相同的同步上下文,必须调用Task实例的ConfigureAwait(false)方法;

await msg.Content.ReadAsStringAsync().ConfigureAwait(false);

异步方法的声明语法与其他方法完全一样,只是要包含async上下文关键字。async可以出

现在返回类型之前的任何位置。async修饰符在生成的代码中没有作用,也可省略不写,它明确表达了你的预期,告诉编译器可以主动寻找await表达式,也可以寻找应该转换成异步调用和await表达式的块调用。

调用者和异步方法之间是通过返回值来通信的。异步函数的返回类型只能为:

Void
、Task、Task<TResult>;Task和Task<TResult>类型都表示一个可能还未完成的操作。
Task<TResult>继承自Task。二者的区别是,Task<TResult>表示一个返回值为T类型的操作,而Task则不需要产生返回值。在某种意义上,你可以认为Task就是Task<void>类型;

之所以将异步方法设计为可以返回void,是为了和事件处理程序兼容。

异步方法签名的约束:所有参数都不能使用out或ref修饰符。

图片 8

 

1.3 使用任务执行基本的操作

在本节中,使用任务执行基本的操作,并且获取任务执行完成后的结果值。本节内容比较简单,在此不做过多介绍。

演示代码如下,在主线程中要获取结果值,常用的方式就是访问task.Result属性,如果任务线程还没执行完毕,那么会阻塞主线程,直到线程执行完。如果任务线程执行完毕,那么将直接拿到运算的结果值。

Task 3中,使用了task.Status来打印线程的状态,线程每个状态的具体含义,将在下一节中介绍。

static void Main(string[] args)
{
    // 直接执行方法 作为参照
    TaskMethod("主线程任务");

    // 访问 Result属性 达到运行结果
    Task<int> task = CreateTask("Task 1");
    task.Start();
    int result = task.Result;
    WriteLine($"运算结果: {result}");

    // 使用当前线程,同步执行任务
    task = CreateTask("Task 2");
    task.RunSynchronously();
    result = task.Result;
    WriteLine($"运算结果:{result}");

    // 通过循环等待 获取运行结果
    task = CreateTask("Task 3");
    WriteLine(task.Status);
    task.Start();

    while (!task.IsCompleted)
    {
        WriteLine(task.Status);
        Sleep(TimeSpan.FromSeconds(0.5));
    }

    WriteLine(task.Status);
    result = task.Result;
    WriteLine($"运算结果:{result}");

    Console.ReadLine();
}

static Task<int> CreateTask(string name)
{
    return new Task<int>(() => TaskMethod(name));
}

static int TaskMethod(string name)
{
    WriteLine($"{name} 运行在线程 {CurrentThread.ManagedThreadId}上. 是否为线程池线程 {CurrentThread.IsThreadPoolThread}");

    Sleep(TimeSpan.FromSeconds(2));

    return 42;
}

运行结果如下,可见Task 1
Task 2均是运行在主线程上,并非线程池线程。

图片 9

 

1.4 组合任务

在本节中,体现了任务其中一个强大的功能,那就是组合任务。通过组合任务可很好的描述任务与任务之间的异步、同步关系,大大降低了编程的难度。

组合任务主要是通过task.ContinueWith()task.WhenAny()task.WhenAll()等和task.GetAwaiter().OnCompleted()方法来实现。

在使用task.ContinueWith()方法时,需要注意它也可传递一系列的枚举选项TaskContinuationOptions,该枚举选项和TaskCreationOptions类似,其具体定义如下表所示。

成员名称 说明
AttachedToParent 如果延续为子任务,则指定将延续附加到任务层次结构中的父级。 只有当延续前面的任务也是子任务时,延续才可以是子任务。 默认情况下,子任务(即由外部任务创建的内部任务)将独立于其父任务执行。 可以使用 TaskContinuationOptions.AttachedToParent 选项以便将父任务和子任务同步。请注意,如果使用 DenyChildAttach 选项配置父任务,则子任务中的 AttachedToParent 选项不起作用,并且子任务将作为分离的子任务执行。有关更多信息,请参见Attached and Detached Child Tasks
DenyChildAttach 指定任何使用 TaskCreationOptions.AttachedToParent 选项创建,并尝试作为附加的子任务执行的子任务(即,由此延续创建的任何嵌套内部任务)都无法附加到父任务,会改成作为分离的子任务执行。 有关详细信息,请参阅附加和分离的子任务
ExecuteSynchronously 指定应同步执行延续任务。 指定此选项后,延续任务在导致前面的任务转换为其最终状态的相同线程上运行。如果在创建延续任务时已经完成前面的任务,则延续任务将在创建此延续任务的线程上运行。 如果前面任务的 CancellationTokenSource 已在一个 finally(在 Visual Basic 中为 Finally)块中释放,则使用此选项的延续任务将在该 finally 块中运行。 只应同步执行运行时间非常短的延续任务。由于任务以同步方式执行,因此无需调用诸如 Task.Wait 的方法来确保调用线程等待任务完成。
HideScheduler 指定由延续通过调用方法(如 Task.RunTask.ContinueWith)创建的任务将默认计划程序 (TaskScheduler.Default) 视为当前的计划程序,而不是正在运行该延续的计划程序。
LazyCancellation 在延续取消的情况下,防止延续的完成直到完成先前的任务。
LongRunning 指定延续将是长期运行的、粗粒度的操作。 它会向 TaskScheduler 提示,过度订阅可能是合理的。
None 如果未指定延续选项,应在执行延续任务时使用指定的默认行为。 延续任务在前面的任务完成后以异步方式运行,与前面任务最终的 Task.Status 属性值无关。 如果延续为子任务,则会将其创建为分离的嵌套任务。
NotOnCanceled 指定不应在延续任务前面的任务已取消的情况下安排延续任务。 如果前面任务完成的 Task.Status 属性是 TaskStatus.Canceled,则前面的任务会取消。 此选项对多任务延续无效。
NotOnFaulted 指定不应在延续任务前面的任务引发了未处理异常的情况下安排延续任务。 如果前面任务完成的 Task.Status 属性是 TaskStatus.Faulted,则前面的任务会引发未处理的异常。 此选项对多任务延续无效。
NotOnRanToCompletion 指定不应在延续任务前面的任务已完成运行的情况下安排延续任务。 如果前面任务完成的 Task.Status 属性是 TaskStatus.RanToCompletion,则前面的任务会运行直至完成。 此选项对多任务延续无效。
OnlyOnCanceled 指定只应在延续前面的任务已取消的情况下安排延续任务。 如果前面任务完成的 Task.Status 属性是 TaskStatus.Canceled,则前面的任务会取消。 此选项对多任务延续无效。
OnlyOnFaulted 指定只有在延续任务前面的任务引发了未处理异常的情况下才应安排延续任务。 如果前面任务完成的 Task.Status 属性是 TaskStatus.Faulted,则前面的任务会引发未处理的异常。OnlyOnFaulted 选项可保证前面任务中的 Task.Exception 属性不是 null。 你可以使用该属性来捕获异常,并确定导致任务出错的异常。 如果你不访问 Exception 属性,则不会处理异常。 此外,如果尝试访问已取消或出错的任务的 Result 属性,则会引发一个新异常。此选项对多任务延续无效。
OnlyOnRanToCompletion 指定只应在延续任务前面的任务已完成运行的情况下才安排延续任务。 如果前面任务完成的 Task.Status 属性是 TaskStatus.RanToCompletion,则前面的任务会运行直至完成。 此选项对多任务延续无效。
PreferFairness 提示 TaskScheduler 按任务计划的顺序安排任务,因此较早安排的任务将更可能较早运行,而较晚安排运行的任务将更可能较晚运行。
RunContinuationsAsynchronously 指定应异步运行延续任务。 此选项优先于 TaskContinuationOptions.ExecuteSynchronously。

演示代码如下所示,使用ContinueWith()OnCompleted()方法组合了任务来运行,搭配不同的TaskCreationOptionsTaskContinuationOptions来实现不同的效果。

static void Main(string[] args)
{
    WriteLine($"主线程 线程 Id {CurrentThread.ManagedThreadId}");

    // 创建两个任务
    var firstTask = new Task<int>(() => TaskMethod("Frist Task",3));
    var secondTask = new Task<int>(()=> TaskMethod("Second Task",2));

    // 在默认的情况下 ContiueWith会在前面任务运行后再运行
    firstTask.ContinueWith(t => WriteLine($"第一次运行答案是 {t.Result}. 线程Id {CurrentThread.ManagedThreadId}. 是否为线程池线程: {CurrentThread.IsThreadPoolThread}"));

    // 启动任务
    firstTask.Start();
    secondTask.Start();

    Sleep(TimeSpan.FromSeconds(4));

    // 这里会紧接着 Second Task运行后运行, 但是由于添加了 OnlyOnRanToCompletion 和 ExecuteSynchronously 所以会由运行SecondTask的线程来 运行这个任务
    Task continuation = secondTask.ContinueWith(t => WriteLine($"第二次运行的答案是 {t.Result}. 线程Id {CurrentThread.ManagedThreadId}. 是否为线程池线程:{CurrentThread.IsThreadPoolThread}"),TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously);

    // OnCompleted 是一个事件  当contiuation运行完成后 执行OnCompleted Action事件
    continuation.GetAwaiter().OnCompleted(() => WriteLine($"后继任务完成. 线程Id {CurrentThread.ManagedThreadId}. 是否为线程池线程 {CurrentThread.IsThreadPoolThread}"));

    Sleep(TimeSpan.FromSeconds(2));
    WriteLine();

    firstTask = new Task<int>(() => 
    {
        // 使用了TaskCreationOptions.AttachedToParent 将这个Task和父Task关联, 当这个Task没有结束时  父Task 状态为 WaitingForChildrenToComplete
        var innerTask = Task.Factory.StartNew(() => TaskMethod("Second Task",5), TaskCreationOptions.AttachedToParent);

        innerTask.ContinueWith(t => TaskMethod("Thrid Task", 2), TaskContinuationOptions.AttachedToParent);

        return TaskMethod("First Task",2);
    });

    firstTask.Start();

    // 检查firstTask线程状态  根据上面的分析 首先是  Running -> WatingForChildrenToComplete -> RanToCompletion
    while (! firstTask.IsCompleted)
    {
        WriteLine(firstTask.Status);

        Sleep(TimeSpan.FromSeconds(0.5));
    }

    WriteLine(firstTask.Status);

    Console.ReadLine();
}

static int TaskMethod(string name, int seconds)
{
    WriteLine($"任务 {name} 正在运行,线程池线程 Id {CurrentThread.ManagedThreadId},是否为线程池线程: {CurrentThread.IsThreadPoolThread}");

    Sleep(TimeSpan.FromSeconds(seconds));

    return 42 * seconds;
}

运行结果如下图所示,与预期结果一致。其中使用了task.Status来打印任务运行的状态,对于task.Status的状态具体含义如下表所示。

成员名称 说明
Canceled 该任务已通过对其自身的 CancellationToken 引发 OperationCanceledException 对取消进行了确认,此时该标记处于已发送信号状态;或者在该任务开始执行之前,已向该任务的 CancellationToken 发出了信号。 有关详细信息,请参阅任务取消
Created 该任务已初始化,但尚未被计划。
Faulted 由于未处理异常的原因而完成的任务。
RanToCompletion 已成功完成执行的任务。
Running 该任务正在运行,但尚未完成。
WaitingForActivation 该任务正在等待 .NET Framework 基础结构在内部将其激活并进行计划。
WaitingForChildrenToComplete 该任务已完成执行,正在隐式等待附加的子任务完成。
WaitingToRun 该任务已被计划执行,但尚未开始执行。

图片 10

线程池相当于线程和用户之间的一个抽象层,向程序员隐藏了使用线程的细节,使得程序员专心处理程序逻辑,而不是各种线程问题。

1.5 将APM模式转换为任务

在前面的章节中,介绍了基于IAsyncResult接口实现了BeginXXXX/EndXXXX方法的就叫APM模式。APM模式非常古老,那么如何将它转换为TAP模式呢?对于常见的几种APM模式异步任务,我们一般选择使用Task.Factory.FromAsync()方法来实现将APM模式转换为TAP模式

演示代码如下所示,比较简单不作过多介绍。

static void Main(string[] args)
{
    int threadId;
    AsynchronousTask d = Test;
    IncompatibleAsychronousTask e = Test;

    // 使用 Task.Factory.FromAsync方法 转换为Task
    WriteLine("Option 1");
    Task<string> task = Task<string>.Factory.FromAsync(d.BeginInvoke("异步任务线程", CallBack, "委托异步调用"), d.EndInvoke);

    task.ContinueWith(t => WriteLine($"回调函数执行完毕,现在运行续接函数!结果:{t.Result}"));

    while (!task.IsCompleted)
    {
        WriteLine(task.Status);
        Sleep(TimeSpan.FromSeconds(0.5));
    }
    WriteLine(task.Status);
    Sleep(TimeSpan.FromSeconds(1));

    WriteLine("----------------------------------------------");
    WriteLine();

    // 使用 Task.Factory.FromAsync重载方法 转换为Task
    WriteLine("Option 2");

    task = Task<string>.Factory.FromAsync(d.BeginInvoke,d.EndInvoke,"异步任务线程","委托异步调用");

    task.ContinueWith(t => WriteLine($"任务完成,现在运行续接函数!结果:{t.Result}"));

    while (!task.IsCompleted)
    {
        WriteLine(task.Status);
        Sleep(TimeSpan.FromSeconds(0.5));
    }
    WriteLine(task.Status);
    Sleep(TimeSpan.FromSeconds(1));

    WriteLine("----------------------------------------------");
    WriteLine();

    // 同样可以使用 FromAsync方法 将 BeginInvoke 转换为 IAsyncResult 最后转换为 Task
    WriteLine("Option 3");

    IAsyncResult ar = e.BeginInvoke(out threadId, CallBack, "委托异步调用");
    task = Task<string>.Factory.FromAsync(ar, _ => e.EndInvoke(out threadId, ar));

    task.ContinueWith(t => WriteLine($"任务完成,现在运行续接函数!结果:{t.Result},线程Id {threadId}"));

    while (!task.IsCompleted)
    {
        WriteLine(task.Status);
        Sleep(TimeSpan.FromSeconds(0.5));
    }
    WriteLine(task.Status);

    ReadLine();
}

delegate string AsynchronousTask(string threadName);
delegate string IncompatibleAsychronousTask(out int threadId);

static void CallBack(IAsyncResult ar)
{
    WriteLine("开始运行回调函数...");
    WriteLine($"传递给回调函数的状态{ar.AsyncState}");
    WriteLine($"是否为线程池线程:{CurrentThread.IsThreadPoolThread}");
    WriteLine($"线程池工作线程Id:{CurrentThread.ManagedThreadId}");
}

static string Test(string threadName)
{
    WriteLine("开始运行...");
    WriteLine($"是否为线程池线程:{CurrentThread.IsThreadPoolThread}");
    Sleep(TimeSpan.FromSeconds(2));

    CurrentThread.Name = threadName;
    return $"线程名:{CurrentThread.Name}";
}

static string Test(out int threadId)
{
    WriteLine("开始运行...");
    WriteLine($"是否为线程池线程:{CurrentThread.IsThreadPoolThread}");
    Sleep(TimeSpan.FromSeconds(2));

    threadId = CurrentThread.ManagedThreadId;
    return $"线程池线程工作Id是:{threadId}";
}

运行结果如下图所示。

图片 11

但是使用线程池也很复杂。有两个问题存在:

1.6 将EAP模式转换为任务

在上几章中有提到,通过BackgroundWorker类通过事件的方式实现的异步,我们叫它EAP模式。那么如何将EAP模式转换为任务呢?很简单,我们只需要通过TaskCompletionSource类,即可将EAP模式转换为任务。

演示代码如下所示。

static void Main(string[] args)
{
    var tcs = new TaskCompletionSource<int>();

    var worker = new BackgroundWorker();
    worker.DoWork += (sender, eventArgs) =>
    {
        eventArgs.Result = TaskMethod("后台工作", 5);
    };

    // 通过此方法 将EAP模式转换为 任务
    worker.RunWorkerCompleted += (sender, eventArgs) =>
    {
        if (eventArgs.Error != null)
        {
            tcs.SetException(eventArgs.Error);
        }
        else if (eventArgs.Cancelled)
        {
            tcs.SetCanceled();
        }
        else
        {
            tcs.SetResult((int)eventArgs.Result);
        }
    };

    worker.RunWorkerAsync();

    // 调用结果
    int result = tcs.Task.Result;

    WriteLine($"结果是:{result}");

    ReadLine();
}

static int TaskMethod(string name, int seconds)
{
    WriteLine($"任务{name}运行在线程{CurrentThread.ManagedThreadId}上. 是否为线程池线程{CurrentThread.IsThreadPoolThread}");

    Sleep(TimeSpan.FromSeconds(seconds));

    return 42 * seconds;
}

运行结果如下图所示。

图片 12

①获取线程池中的工作线程的结果比较难

1.7 实现取消选项

在TAP模式中,实现取消选项和之前的异步模式一样,都是使用CancellationToken来实现,但是不同的是Task构造函数允许传入一个CancellationToken,从而在任务实际启动之前取消它。

演示代码如下所示。

static void Main(string[] args)
{
    var cts = new CancellationTokenSource();
    // new Task时  可以传入一个 CancellationToken对象  可以在线程创建时  变取消任务
    var longTask = new Task<int>(() => TaskMethod("Task 1", 10, cts.Token), cts.Token);
    WriteLine(longTask.Status);
    cts.Cancel();
    WriteLine(longTask.Status);
    WriteLine("第一个任务在运行前被取消.");

    // 同样的 可以通过CancellationToken对象 取消正在运行的任务
    cts = new CancellationTokenSource();
    longTask = new Task<int>(() => TaskMethod("Task 2", 10, cts.Token), cts.Token);
    longTask.Start();

    for (int i = 0; i < 5; i++)
    {
        Sleep(TimeSpan.FromSeconds(0.5));
        WriteLine(longTask.Status);
    }
    cts.Cancel();
    for (int i = 0; i < 5; i++)
    {
        Sleep(TimeSpan.FromSeconds(0.5));
        WriteLine(longTask.Status);
    }

    WriteLine($"这个任务已完成,结果为{longTask.Result}");

    ReadLine();
}

static int TaskMethod(string name, int seconds, CancellationToken token)
{
    WriteLine($"任务运行在{CurrentThread.ManagedThreadId}上. 是否为线程池线程:{CurrentThread.IsThreadPoolThread}");

    for (int i = 0; i < seconds; i++)
    {
        Sleep(TimeSpan.FromSeconds(1));
        if (token.IsCancellationRequested)
        {
            return -1;
        }
    }

    return 42 * seconds;
}

运行结果如下图所示,这里需要注意的是,如果是在任务执行之前取消了任务,那么它的最终状态是Canceled。如果是在执行过程中取消任务,那么它的状态是RanCompletion

图片 13

②实现线程池中工作线程执行的时序问题

1.8 处理任务中的异常

在任务中,处理异常和其它异步方式处理异常类似,如果能在所发生异常的线程中处理,那么不要在其它地方处理。但是对于一些不可预料的异常,那么可以通过几种方式来处理。

可以通过访问task.Result属性来处理异常,因为访问这个属性的Get方法会使当前线程等待直到该任务完成,并将异常传播给当前线程,这样就可以通过try catch语句块来捕获异常。另外使用task.GetAwaiter().GetResult()方法和第使用task.Result类似,同样可以捕获异常。如果是要捕获多个任务中的异常错误,那么可以通过ContinueWith()方法来处理。

具体如何实现,演示代码如下所示。

static void Main(string[] args)
{
    Task<int> task;
    // 在主线程中调用 task.Result task中的异常信息会直接抛出到 主线程中
    try
    {
        task = Task.Run(() => TaskMethod("Task 1", 2));
        int result = task.Result;
        WriteLine($"结果为: {result}");
    }
    catch (Exception ex)
    {
        WriteLine($"异常被捕捉:{ex.Message}");
    }
    WriteLine("------------------------------------------------");
    WriteLine();

    // 同上 只是访问Result的方式不同
    try
    {
        task = Task.Run(() => TaskMethod("Task 2", 2));
        int result = task.GetAwaiter().GetResult();
        WriteLine($"结果为:{result}");
    }
    catch (Exception ex)
    {
        WriteLine($"异常被捕捉: {ex.Message}");
    }
    WriteLine("----------------------------------------------");
    WriteLine();

    var t1 = new Task<int>(() => TaskMethod("Task 3", 3));
    var t2 = new Task<int>(() => TaskMethod("Task 4", 4));

    var complexTask = Task.WhenAll(t1, t2);
    // 通过ContinueWith TaskContinuationOptions.OnlyOnFaulted的方式 如果task出现异常 那么才会执行该方法
    var exceptionHandler = complexTask.ContinueWith(t => {
        WriteLine($"异常被捕捉:{t.Exception.Message}");
        foreach (var ex in t.Exception.InnerExceptions)
        {
            WriteLine($"-------------------------- {ex.Message}");
        }
    },TaskContinuationOptions.OnlyOnFaulted);

    t1.Start();
    t2.Start();

    ReadLine();
}

static int TaskMethod(string name, int seconds)
{
    WriteLine($"任务运行在{CurrentThread.ManagedThreadId}上. 是否为线程池线程:{CurrentThread.IsThreadPoolThread}");

    Sleep(TimeSpan.FromSeconds(seconds));
    // 人为抛出一个异常
    throw new Exception("Boom!");
    return 42 * seconds;
}

运行结果如下所示,需要注意的是,如果在ContinueWith()方法中捕获多个任务产生的异常,那么它的异常类型是AggregateException,具体的异常信息包含在InnerExceptions里面,要注意和InnerException区分。

图片 14

综上,我们在第3章中提过的异步编程模型和基于事件的异步编程模型,这些模式使得获取结果更加容易,传播也更轻松,但是在进行多个异步操作组合的时候,还需要编写大量的代码。对于第二个问题.NET 4.0提出了一个新的关于异步操作的API。叫做任务并行库(Task Parallel Library
简称 TPL)。

1.9 并行运行任务

本节中主要介绍了两个方法的使用,一个是等待组中全部任务都执行结束的Task.WhenAll()方法,另一个是只要组中一个方法执行结束都执行的Task.WhenAny()方法。

具体使用,如下演示代码所示。

static void Main(string[] args)
{
    // 第一种方式 通过Task.WhenAll 等待所有任务运行完成
    var firstTask = new Task<int>(() => TaskMethod("First Task", 3));
    var secondTask = new Task<int>(() => TaskMethod("Second Task", 2));

    // 当firstTask 和 secondTask 运行完成后 才执行 whenAllTask的ContinueWith
    var whenAllTask = Task.WhenAll(firstTask, secondTask);
    whenAllTask.ContinueWith(t => WriteLine($"第一个任务答案为{t.Result[0]},第二个任务答案为{t.Result[1]}"), TaskContinuationOptions.OnlyOnRanToCompletion);

    firstTask.Start();
    secondTask.Start();

    Sleep(TimeSpan.FromSeconds(4));

    // 使用WhenAny方法  只要列表中有一个任务完成 那么该方法就会取出那个完成的任务
    var tasks = new List<Task<int>>();
    for (int i = 0; i < 4; i++)
    {
        int counter = 1;
        var task = new Task<int>(() => TaskMethod($"Task {counter}",counter));
        tasks.Add(task);
        task.Start();
    }

    while (tasks.Count > 0)
    {
        var completedTask = Task.WhenAny(tasks).Result;
        tasks.Remove(completedTask);
        WriteLine($"一个任务已经完成,结果为 {completedTask.Result}");
    }

    ReadLine();
}

static int TaskMethod(string name, int seconds)
{
    WriteLine($"任务运行在{CurrentThread.ManagedThreadId}上. 是否为线程池线程:{CurrentThread.IsThreadPoolThread}");

    Sleep(TimeSpan.FromSeconds(seconds));
    return 42 * seconds;
}

运行结果如下图所示。

图片 15

 

1.10 使用TaskScheduler配置任务执行

Task中,负责任务调度是TaskScheduler对象,FCL提供了两个派生自TaskScheduler的类型:线程池任务调度器(Thread
Pool Task Scheduler)
同步上下文任务调度器(Synchronization
Scheduler)
。默认情况下所有应用程序都使用线程池任务调度器,但是在UI组件中,不使用线程池中的线程,避免跨线程更新UI,需要使用同步上下文任务调度器。可以通过执行TaskSchedulerFromCurrentSynchronizationContext()静态方法来获得对同步上下文任务调度器的引用。

演示程序如下所示,为了延时同步上下文任务调度器,我们此次使用WPF来创建项目。

MainWindow.xaml 代码如下所示。

<Window x:Class="Recipe9.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Recipe9"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <TextBlock Name="ContentTextBlock" HorizontalAlignment="Left" Margin="44,134,0,0" VerticalAlignment="Top" Width="425" Height="40"/>
        <Button Content="Sync" HorizontalAlignment="Left" Margin="45,190,0,0" VerticalAlignment="Top" Width="75" Click="ButtonSync_Click"/>
        <Button Content="Async" HorizontalAlignment="Left" Margin="165,190,0,0" VerticalAlignment="Top" Width="75" Click="ButtonAsync_Click"/>
        <Button Content="Async OK" HorizontalAlignment="Left" Margin="285,190,0,0" VerticalAlignment="Top" Width="75" Click="ButtonAsyncOK_Click"/>
    </Grid>
</Window>

MainWindow.xaml.cs 代码如下所示。

/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    // 同步执行 计算密集任务 导致UI线程阻塞
    private void ButtonSync_Click(object sender, RoutedEventArgs e)
    {
        ContentTextBlock.Text = string.Empty;

        try
        {
            string result = TaskMethod().Result;
            ContentTextBlock.Text = result;
        }
        catch (Exception ex)
        {
            ContentTextBlock.Text = ex.InnerException.Message;
        }
    }

    // 异步的方式来执行 计算密集任务 UI线程不会阻塞 但是 不能跨线程更新UI 所以会有异常
    private void ButtonAsync_Click(object sender, RoutedEventArgs e)
    {
        ContentTextBlock.Text = string.Empty;
        Mouse.OverrideCursor = Cursors.Wait;

        Task<string> task = TaskMethod();
        task.ContinueWith(t => {
            ContentTextBlock.Text = t.Exception.InnerException.Message;
            Mouse.OverrideCursor = null;
        }, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.FromCurrentSynchronizationContext());
    }

    // 通过 异步 和 FromCurrentSynchronizationContext方法 创建了线程同步的上下文  没有跨线程更新UI 
    private void ButtonAsyncOK_Click(object sender, RoutedEventArgs e)
    {
        ContentTextBlock.Text = string.Empty;
        Mouse.OverrideCursor = Cursors.Wait;
        Task<string> task = TaskMethod(TaskScheduler.FromCurrentSynchronizationContext());

        task.ContinueWith(t => Mouse.OverrideCursor = null,
            CancellationToken.None,
            TaskContinuationOptions.None,
            TaskScheduler.FromCurrentSynchronizationContext());
    }

    Task<string> TaskMethod()
    {
        return TaskMethod(TaskScheduler.Default);
    }

    Task<string> TaskMethod(TaskScheduler scheduler)
    {
        Task delay = Task.Delay(TimeSpan.FromSeconds(5));

        return delay.ContinueWith(t =>
        {
            string str = $"任务运行在{CurrentThread.ManagedThreadId}上. 是否为线程池线程:{CurrentThread.IsThreadPoolThread}";

            Console.WriteLine(str);

            ContentTextBlock.Text = str;
            return str;
        }, scheduler);
    }
}

运行结果如下所示,从左至右依次单击按钮,前两个按钮将会引发异常。
图片 16

具体信息如下所示。

图片 17

TPL可以看成线程池之上的又一个抽象层,其对程序员隐藏了与线程池交互的底层代码,并提供了更方便的细粒度的API。

参考书籍

本文主要参考了以下几本书,在此对这些作者表示由衷的感谢,感谢你们为.Net的发扬光大所做的贡献!

  1. 《CLR via C#》
  2. 《C# in Depth Third Edition》
  3. 《Essential C# 6.0》
  4. 《Multithreading with C# Cookbook Second Edition》
  5. 《C#多线程编程实战》

源码下载点击链接
示例源码下载

TPL的核心概念是任务。一个任务代表了一个异步操作,该操作可以使用多种方式运行,可以使用或不使用独立线程运行。

笔者水平有限,如果错误欢迎各位批评指正!

本来想趁待业期间的时间读完《Multithreading with C# Cookbook Second
Edition》这本书,并且分享做的相关笔记;但是由于笔者目前职业规划和身体原因,可能最近都没有时间来更新这个系列,没法做到几天一更。请大家多多谅解!但是笔者一定会将这个系列全部更新完成的!感谢大家的支持!

一个任务可以有多种方式和其他任务组合起来。例如,可以同时执行多个任务,等待所有任务完成,然后运行一个任务对之前所有的任务结果进行一些计算。TPL与之前的模式相比,其中一个关键优势是其具有用于组合任务的便利的API。

处理任务中的异常结果也有多种方式。一个任务可以由多种任务组成,这些任务也可以有各自的子任务,所以有一个AggregateException的概念。这种异常可以捕获底层任务内部的所有异常,并允许单独处理这些异常。

C#5.0中可以使用await和async关键词以平滑的,舒服的方式进行操作任务。

相关文章

No Comments, Be The First!
近期评论
    功能
    网站地图xml地图