C# 多线程(4)Task的使用

一、Task的机制

在C#4.0之前需要执行一个复杂的异步操作时,只能使用CLR线程池技术来执行一个任务。线程池执行异步任务时,不知道任务何时完成,以及任务的在任务完成后不能获取到返回值。但是在C#4.0中引人了一个的任务(System.Threading.Tasks命名空间的类型)机制来解决异步操作完成时间和完成后返回值的问题。

其实现机制大致类似于线程池ThreadPool,不过对于ThreadPool来说Task的优势是很明显的:

ThreadPool的实现机制:(一对多
1)应用程序拥有一个用于存放委托的全局队列;
2)使用ThreadPool.QueueUserWorkItem将新的委托加入到全局队列;
3)线程池中的多个线程按照先进先出的方式取出委托并执行。

Task的实现机制:(多对多
1)应用程序拥有一个用于存放Task(包装的委托)的全局队列(存放主程序创建的Task,标记为了TaskCreationOptions.PreferFairness的Task),以及线程池中每个工作线程对应的本地队列(存放该工作线程创建的Task);
2)使用new Task()或Task.Factory.StartNew将新的Task加入到指定队列;
3)线程池中的多个线程按照优先处理本地队列,其次处理全局队列的方式取出Task并执行;
4)如果工作线程A发现本地队列为空(Task已处理完毕),那么A就会尝试去全局队列中获取Task,如果全局队列也为空,那么A就会到工作线程B的本地队列中“窃取”一个Task来执行,这种策略很明显的使得CPU更加充分的利用了并行执行。

二、Task的使用

2.1 使用Task类创建并执行简单任务

通过使用Task的构造函数来创建任务,并调用Start方法来启动任务并执行异步操作。创建任务时,必须传递一个Action或Action< object>类型的委托回调方法,可以选择的传递任务执行时说需要的数据对象等。
Task类的构造函数如下:

public Task(Action action);
public Task(Action<object> action, object state);
public Task(Action action, CancellationToken cancellationToken);
public Task(Action action, TaskCreationOptions creationOptions);
public Task(Action<object> action, object state, CancellationToken cancellationToken);
public Task(Action<object> action, object state, TaskCreationOptions creationOptions);
public Task(Action action, CancellationToken cancellationToken, 
TaskCreationOptions creationOptions);
public Task(Action<object> action, object state, 
CancellationToken cancellationToken, TaskCreationOptions creationOptions);

示例代码:

//新建一个Task
Task t1 = new Task(() => {
     Console.WriteLine("Task完成!");
});
//启动Task
t1.Start();
Console.WriteLine("UI线程完成!");

上面是用new关键字创建,等同于如下使用Task.Factory(Task工厂)创建的方式:

//新建一个Task(Task工厂创建,自动启动)
Task t1 = Task.Factory.StartNew(() => {
      Console.WriteLine("Task完成!");
});
Console.WriteLine("UI线程完成!");

这里为了简便使用了Lambda表达式(=> 为Lambda运算符),上面两部分代码都等同于如下:

void Test()
{
   Console.WriteLine("Task完成!");
}

Action action = new Action(Test);
//新建一个Task
Task t1 = new Task(action);
//启动Task
t1.Start();
Console.WriteLine("UI线程完成!");

Task的执行方式有同步和异步两种,上面的方式很明显是异步执行,我们可以看到做为主线程的UI线程是先一步执行完的。那么要怎么样才能实现Task的同步执行呢?主要就这一个方法:Wait()
代码如下:

 //新建一个Task
 Task t1 = new Task(() => {
      Console.WriteLine("Task完成!");
 });
 //启动Task
 t1.Start();

 Console.WriteLine("UI线程开始等待!"); //①
 t1.Wait();
 Console.WriteLine("UI线程完成!"); //②

主线程运行到t1.Wait()时,会读取t1的状态,当发现任务t1还未执行结束时,主线程便会阻塞在这个位置(只是阻塞在t1.Wait()位置,也就是说t1.Wait()之前的代码①照旧执行),当读取到t1的状态为已经执行结束时,主线程才会再次恢复执行,从t1.Wait()之后的位置②继续往下执行。

当然,当有多个任务都需要保持同步执行时,可以使用Task.WaitAll方法同时等待多个任务完成,代码如下:

 //新建一个Task
 Task t1 = new Task(() => {
      Console.WriteLine("Task1完成!");
  });
 //新建一个Task
 Task t2 = new Task(() => {
    Console.WriteLine("Task2完成!");
 });
 //启动Task
 t1.Start();
 t2.Start();

 Console.WriteLine("UI线程开始等待!");
 //等待t1,t2都完成
 Task.WaitAll(t1,t2);
 Console.WriteLine("UI线程完成!");

Task类还有一些静态方法,WaitAll用于等待提供的所有 System.Threading.Tasks.Task 对象完成执行过程和Wait用于等待提供的任一个 System.Threading.Tasks.Task 对象完成执行过程,这两个方法都有一些重载版本。

//等待所有任务完成  
public static void WaitAll(params Task[] tasks);
//等待任意一个任务完成
public static int WaitAny(params Task[] tasks);

2.2 等待任务的完成并获取返回值

使用任务执行异步操作时,最主要的是要后的任务完成时的返回值。在任务类中有一个实例方法Wait(有许多重载版本)他能等待任务的完成,我们也可以通过Task类的派生类Task< TResult>创建一个异步任务,并指定任务完成时返回值的类型,这样可以通过Task< TResult>的实例对象获取到任务完成后的返回值。
创建一个异步任务并执行0到100求和操作返回最后的计算结果,示例代码:

 static void TaskWait() 
 {
          //创建任务
          Task<int> task = new Task<int>(() =>
          {
                int sum = 0;
                Console.WriteLine("使用Task执行异步操作.");
               for (int i = 0; i < 100; i++)
               {
                   sum+=i;
              }
              return sum;
          });
          //启动任务,并安排到当前任务队列线程中执行任务(System.Threading.Tasks.TaskScheduler)
          task.Start();

          Console.WriteLine("主线程执行其他处理");
          //等待任务的完成执行过程。
          task.Wait();
          //获得任务的执行结果
          Console.WriteLine("任务执行结果:{0}", task.Result.ToString());
  }

2.3 使用ContinueWith方法在任务完成时启动一个新任务

在使用能够Task类的Wait方法等待一个任务时或派生类的Result属性获得任务执行结果都有可能阻塞线程,为了解决这个问题可以使用ContinueWith方法,他能在一个任务完成时自动启动一个新的任务来处理执行结果。

示例代码:

static void TaskContinueWith()
 {
        //创建一个任务
       Task<int> task = new Task<int>(() =>
       {
           int sum = 0;
             Console.WriteLine("使用Task执行异步操作.");
           for (int i = 0; i < 100; i++)
            {
               sum += i;
            }
            return sum;
       });
        //启动任务,并安排到当前任务队列线程中执行任务(System.Threading.Tasks.TaskScheduler)
         task.Start();
         Console.WriteLine("主线程执行其他处理");
         //任务完成时执行处理。
        Task cwt = task.ContinueWith(t => { 
              Console.WriteLine("任务完成后的执行结果:{0}", t.Result.ToString()); 
           });
         Thread.Sleep(1000);
  }

上述示例中任务不是等待完成来显示执行结果,而是**使用ContinueWith方法,它能够知道任务在什么时候完成并启动一个新的任务来执行任务完成后的处理。**ContinueWith方法具有一些重载版本,这些重载版本允许指定延续任务需要使用的数据、延续任务的工作方式(System.Threading.Tasks.TaskContinuationOptions的枚举值按位OR运行的结果)等。

2.4 创建父子任务和任务工厂的使用

通过Task类创建的任务是顶级任务,可以通过使用 TaskCreationOptions.AttachedToParent 标识把这些任务与创建他的任务相关联,所有子任务全部完成以后父任务才会结束操作。示例如下:

static void ParentChildTask() 
{
    Task<string[]> parent = new Task<string[]>(state => {
         Console.WriteLine(state);
         string[] result=new string[2];
         //创建并启动子任务
         new Task(() => { result[0]= "我是子任务1。";},
         TaskCreationOptions.AttachedToParent).Start();
         new Task(() => { result[1] = "我是子任务2。"; }, 
         TaskCreationOptions.AttachedToParent).Start();

         return result;
     },"我是父任务,并在我的处理过程中创建多个子任务,所有子任务完成以后我才会结束执行。");
     //任务处理完成后执行的操作
     parent.ContinueWith(t => {
         Array.ForEach(t.Result, r=>Console.WriteLine(r));
     });
     //启动父任务
     parent.Start();
     Console.Read();
 }

如果需要创建一组具有相同状态的任务时,可以使用TaskFactory类或TaskFactory类。这两个类创建一组任务时可以指定任务的CancellationToken、TaskCreationOptions、TaskContinuationOptions和TaskScheduler默认值。示例代码:

 static void TaskFactoryApply()         
 {
      Task parent = new Task(() =>
      {
          CancellationTokenSource cts = new CancellationTokenSource(5000);
          //创建任务工厂
          TaskFactory tf = new TaskFactory(cts.Token, TaskCreationOptions.AttachedToParent, 
          TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
          //添加一组具有相同状态的子任务
          Task[] task = new Task[]{
              tf.StartNew(() => { Console.WriteLine("我是任务工厂里的第一个任务。"); }),
              tf.StartNew(() => { Console.WriteLine("我是任务工厂里的第二个任务。"); }),
              tf.StartNew(() => { Console.WriteLine("我是任务工厂里的第三个任务。"); })
          };
      });
      parent.Start();
      Console.Read();
 }

2.5 取消任务

我们知道task是并行计算的,比如说主线程在某个时刻由于某种原因要取消某个task的执行,我们能做到吗? 当然我们可以做到。在4.0中给我们提供一个“取消标记”叫做CancellationTokenSource.Token,在创建task的时候传入此参数,就可以将主线程和任务相
关联,然后在任务中设置“取消信号“叫做ThrowIfCancellationRequested来等待主线程使用Cancel来通知,一旦cancel被调用。task将会抛出OperationCanceledException来中断此任务的执行,最后将当前task的Status的IsCanceled属性设为true。看起来是不是很抽象,没关系,上代码说话。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
class Program
{
    static void Main(string[] args)
    {
        var cts = new CancellationTokenSource();
        var ct = cts.Token;

        Task task1 = new Task(() => { Run1(ct); }, ct);
        Task task2 = new Task(Run2);

        try
        {
            task1.Start();
            task2.Start();

            Thread.Sleep(1000);
            cts.Cancel();
            Task.WaitAll(task1, task2);
        }
        catch (AggregateException ex)
        {
            foreach (var e in ex.InnerExceptions)
            {
                Console.WriteLine("\n hi,我是OperationCanceledException:{0}\n", e.Message);
            }

            //task1是否取消
            Console.WriteLine("task1是不是被取消了? {0}", task1.IsCanceled);
            Console.WriteLine("task2是不是被取消了? {0}", task2.IsCanceled);
        }

        Console.Read();
    }

    static void Run1(CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();
        Console.WriteLine("我是任务1");
        Thread.Sleep(2000);
        ct.ThrowIfCancellationRequested();
        Console.WriteLine("我是任务1的第二部分信息");
    }

    static void Run2()
    {
        Console.WriteLine("我是任务2");
    }
}

运行结果分析:
1)Run1中的Console.WriteLine(“我是任务1的第二部分信息”); 没有被执行。
2)Console.WriteLine(“task1是不是被取消了? {0}”, task1.IsCanceled); 状态为True。

也就告诉我们Run1中途被主线程中断执行,我们coding的代码起到效果了。

2.6 任务内部实现和任务调度

任务内部有一组构成任务状态的属性,标识任务的唯一Id、表示任务的执行状态(TaskStatus)、任务创建时提供的回调函数的引用和传递给回调函数的数据对象AsyncState、对任务创建时的任务调度对象(TaskScheduler)的引用、对父任务的引用以及对执行上下文的引用和ManualResetEventSlim对象的引用。Task类和Task类都实现了标准的释放资源的接口,允许在任务完成处理的时候使用Dispose方法释放资源(关闭ManualResetEventSlim对象实例)。可以使用Task类的CurrentId属性获得正在执行的任务的Id,如果没有任务在执行CurrentId返回值为null,CurrentId是一个int?可空类型的属性。任务执行的生命周期通过TaskStatus类型的一个值来表示,TaskStatus所包含的值:

public enum TaskStatus
{
        Created = 0,
        WaitingForActivation = 1,
        WaitingToRun = 2,
        Running = 3,
        WaitingForChildrenToComplete = 4,
        RanToCompletion = 5,
        Canceled = 6,
        Faulted = 7,
}

我们可以通过Task类的Exception属性获得任务在执行过程中的所有异常

,Exception是一个AggregateException类型的属性。Task类提供了IsCanceled、IsCompleted、IsFaulted属性来获得任务的完成状态。通过ContinueWith、ContinueWhenAll、ContinueWhenAny和FromAsync创建的后续任务都处于WaitingForActivation 状态,这个状态的任务会在父任务完成后自动执行。

在任务内部由TaskScheduler类调度任务的执行,该类是一个抽象类,FCL中从他派生了两个派生类:ThreadPoolTaskScheduler线程池任务调度器和SynchronizationContextTaskScheduler同步上下文任务调度器。所有任务默认都是采用ThreadPoolTaskScheduler调度任务,他是采用线程池来执行任务,可以通过TaskScheduler类的静态属性Default获得对默认任务调度器的引用。SynchronizationContextTaskScheduler任务调度器能够用在Window form、WPF等应用程序,他的任务调度是采用的GUI线程,所以他能同步更新UI组件,可以通过TaskScheduler类的静态方法FromCurrentSynchronizationContext获得对一个同步上下文任务调度起的引用。

任务调度示例:

 private void button1_Click(object sender, EventArgs e)
 {
    //获得同步上下文任务调度器
    TaskScheduler m_syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

     //创建任务,并采用默认任务调度器(线程池任务调度器)执行任务
     Task<int> task = new Task<int>(() =>
     {
         //执行复杂的计算任务。
         Thread.Sleep(2000);
         int sum = 0;
         for (int i = 0; i < 100; i++)
         {
             sum += i;
         }
         return sum;
     });
      var cts=new CancellationTokenSource();
     //任务完成时启动一个后续任务,并采用同步上下文任务调度器调度任务更新UI组件。
     task.ContinueWith(t => {
     this.label1.Text="采用SynchronizationContextTaskScheduler任务调度器更新UI。\r\n计算结果是:"+task.Result.ToString(); },
     cts.Token ,
     TaskContinuationOptions.AttachedToParent,m_syncContextTaskScheduler);
     task.Start();
 }

本文简单的介绍了使用Task类来执行异步操作以及任务的内部实现与任务调度。在执行复杂异步操作时,可以采用任务来执行,他能更好的知道异步操作在何时完成以及返回异步操作的执行结果。

阅读更多 登录后自动展开
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页