编写高质量代码改善C#程序的157个建议——建议80:用Task代替ThreadPool

建议80:用Task代替ThreadPool

ThreadPool相对于Thread来说具有很多优势,但是ThreadPool在使用上却存在一定的不方便。比如:

ThreadPool不支持线程的取消、完成、失败通知等交互性操作。

ThreadPool不支持线程执行的先后次序。

以往,如果开发者要实现上述功能,需要完成很多额外的工作。现在,FCL中提供了一个功能更强大的概念:Task。Task在线程池的基础上进行了优化,并提供了更多的API。在FCL 4.0中,如果我们要编写多线程程序,Task显然已经优于传统的方式了。

以下是一个简单的任务示例:

static void Main(string[] args)
{
    Task t = new Task(() =>
        {
            Console.WriteLine("任务开始工作……");
            //模拟工作过程
            Thread.Sleep(5000);
        });
    t.Start();
    t.ContinueWith((task) =>
        {
            Console.WriteLine("任务完成,完成时候的状态为:");
            Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}",
     task.IsCanceled, task.IsCompleted, task.IsFaulted);
        });
    Console.ReadKey();
} 

任务Task具备以下属性,可以让我们查询任务完成时的状态:

IsCanceled  因为被取消而完成  
IsCompleted 成功完成  
IsFaulted       因为发生异常而完成
需要注意的是,任务并没有提供回调事件来通知完成(像BackgroundWorker一样),它是通过启用一个新任务的方式来完成类似的功能。ContinueWith方法可以在一个任务完成的时候发起一个新任务,这种方式天然就支持了任务的完成通知:我们可以在新任务中获取原任务的结果值。

下面是一个稍微复杂的例子,同时支持完成通知、取消、获取任务返回值等功能:

static void Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();
    Task<int> t = new Task<int>(() => Add(cts.Token), cts.Token);
    t.Start();
    t.ContinueWith(TaskEnded);
    //等待按任意键取消任务
    Console.ReadKey();
    cts.Cancel();
    Console.ReadKey();
}  

static void TaskEnded(Task<int> task)
{
    Console.WriteLine("任务完成,完成时候的状态为:");
    Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}",
        task.IsCanceled, task.IsCompleted, task.IsFaulted);
    Console.WriteLine("任务的返回值为:{0}", task.Result);
}  

static int Add(CancellationToken ct)
{
    Console.WriteLine("任务开始……");
    int result = 0;
    while (!ct.IsCancellationRequested)
    {
        result++;
        Thread.Sleep(1000);
    }
    return result;
} 

在任务开始后大概3秒的时候按下键盘,会得到如下的输出:
任务开始……
任务完成,完成时候的状态为:
IsCanceled=False        IsCompleted=True        IsFaulted=False
任务的返回值为:3

你也许会奇怪,我们的任务是通过Cancel的方式处理的,为什么完成的状态IsCanceled那一栏还是False。因为在工作任务中,我们对IsCancellationRequested进行了业务逻辑上的处理,但是并没有通过ThrowIfCancellationRequested方法来处理。如果采用ThrowIfCancellationRequested方法,则代码应如下所示:

static void Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();
    Task<int> t = new Task<int>(() => AddCancleByThrow(cts.Token), cts.Token);
    t.Start();
    t.ContinueWith(TaskEndedByCatch);
    //等待按任意键取消任务
    Console.ReadKey();
    cts.Cancel();
    Console.ReadKey();
}  

static void TaskEndedByCatch(Task<int> task)
{
    Console.WriteLine("任务完成,完成时候的状态为:");
    Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}",
        task.IsCanceled, task.IsCompleted, task.IsFaulted);
    try
    {
        Console.WriteLine("任务的返回值为:{0}", task.Result);
    }
    catch (AggregateException e)
    {
        e.Handle((err) => err is OperationCanceledException);
    }
}  

static int AddCancleByThrow(CancellationToken ct)
{
    Console.WriteLine("任务开始……");
    int result = 0;
    while (true)
    {
        ct.ThrowIfCancellationRequested();
        result++;
        Thread.Sleep(1000);
    }
    return result;
} 

那么输出为:
任务开始……
任务完成,完成时候的状态为:
IsCanceled=True     IsCompleted=True        IsFaulted=False

在任务结束求值的方法TaskEndedByCatch中,如果任务是通过ThrowIfCancellation Requested方法结束的,对任务求结果值将会抛出异常OperationCanceledException,而不是得到抛出异常前的结果值。这意味着任务是通过异常的方式被取消掉的,所以可以注意到上面代码的输出中,状态IsCanceled为True。

接着来看上面的输出,我们注意到取消是通过异常的方式实现的,而表示任务中发生了异常的IsFaulted状态却还是为False,为什么呢?这是因为ThrowIfCancellation Requested是协作式取消方式的类型CancellationTokenSource中的一个方法,CLR对其进行了特殊的处理。CLR知道这一行程序是开发者有意为之,所以不把它看做是一个异常(它被理解为取消)。要得到IsFaulted等于True的状态,我们可以修改While循环,模拟一个异常出来,具体方法如下:

while (true)
{
//ct.ThrowIfCancellationRequested();
if (result == 5)
{
    throw new Exception("error");
}
result++;
Thread.Sleep(1000);
} 

模拟异常后的输出为:

任务开始……
任务完成,完成时候的状态为:
IsCanceled=False        IsCompleted=True        IsFaulted=True

Task还支持任务工厂的概念。任务工厂支持多个任务之间共享相同的状态,如取消类型CancellationTokenSource就是可以被共享的。通过使用任务工厂,可以同时取消一组任务:

static void Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();
    //等待按任意键取消任务
    TaskFactory taskFactory = new TaskFactory();
    Task[] tasks = new Task[]
        {
            taskFactory.StartNew(() => Add(cts.Token)),
            taskFactory.StartNew(() => Add(cts.Token)),
            taskFactory.StartNew(() => Add(cts.Token))
        };
    //CancellationToken.None指示TasksEnded不能被取消
    taskFactory.ContinueWhenAll(tasks, TasksEnded, CancellationToken.None);
    Console.ReadKey();
    cts.Cancel();
    Console.ReadKey();
}  

static void TasksEnded(Task[] tasks)
{
    Console.WriteLine("所有任务已完成!");
} 

以上代码的输出为:

任务开始……  
任务开始……  
任务开始……  
所有任务已完成(取消)!

本建议演示了Task(任务)和TaskFactory(任务工厂)的使用方法。Task进一步优化了后台线程池的调度,加快了线程的处理速度。所以在FCL 4.0时代,如果要使用多线程,我们理应更多地使用Task。

因此,在本书接下来的建议中,如无特别必要,只要涉及多线程内容的,都将一并使用Task来完成。

转自:《编写高质量代码改善C#程序的157个建议》陆敏技

时间: 2024-07-31 18:21:13

编写高质量代码改善C#程序的157个建议——建议80:用Task代替ThreadPool的相关文章

编写高质量代码改善C#程序的157个建议——建议45:为泛型类型参数指定逆变

建议45:为泛型类型参数指定逆变 逆变是指方法的参数可以是委托或者泛型接口的参数类型的基类.FCL4.0中支持逆变的常用委托有: Func<int T,out TResult> Predicate<in T> 常用委托有: IComparer<in T> 下面例子演示了泛型类型参数指定逆变所带来的好处: class Program { static void Main() { Programmer p = new Programmer { Name = "Mi

编写高质量代码改善C#程序的157个建议——建议27:在查询中使用Lambda表达式

建议27:在查询中使用Lambda表达式 LINQ实际上是基于扩展方法和Lambda表达式的.任何LINQ查询都能通过扩展方法的方式来代替. var personWithCompanyList = from person in personList select new { PersonName = person.Name, CompanyName = person.CompanyID==0?"Micro":"Sun" }; foreach (var item in

编写高质量代码改善C#程序的157个建议——建议26:使用匿名类型存储LINQ查询结果

建议26:使用匿名类型存储LINQ查询结果 从.NET3.0开始,C#开始支持一个新特性:匿名类型.匿名类型有var.赋值运算符和一个非空初始值(或以new开头的初始化项)组成.匿名类型有如下基本特性: 即支持简单类型也指出复杂类型.简单类型必须是一个非空初始值,复杂类型则是一个以new开头的初始化项. 匿名类型的属性是只读的,没有属性设置器,它一旦被初始化就不可更改. 如果两个匿名类型的属性值相同,那么就认为这两个匿名类型相等. 匿名类型可以再循环中用作初始化器. 匿名类型支持智能感知. 匿名

编写高质量代码改善C#程序的157个建议——建议20:使用泛型集合代替非泛型集合

建议20:使用泛型集合代替非泛型集合 在建议1中我们知道,如果要让代码高效运行,应该尽量避免装箱和拆箱,以及尽量减少转型.很遗憾,在微软提供给我们的第一代集合类型中没有做到这一点,下面我们看ArrayList这个类的使用情况: ArrayList al=new ArrayList(); al.Add(0); al.Add(1); al.Add("mike"); foreach (var item in al) { Console.WriteLine(item); } 上面这段代码充分演

编写高质量代码改善C#程序的157个建议——建议12: 重写Equals时也要重写GetHashCode

建议12: 重写Equals时也要重写GetHashCode 除非考虑到自定义类型会被用作基于散列的集合的键值:否则,不建议重写Equals方法,因为这会带来一系列的问题. 如果编译上一个建议中的Person这个类型,编译器会提示这样一个信息: “重写 Object.Equals(object o)但不重写 Object.GetHashCode()” 如果重写Equals方法的时候不重写GetHashCode方法,在使用如FCL中的Dictionary类时,可能隐含一些潜在的Bug.还是针对上一

编写高质量代码改善C#程序的157个建议——建议13: 为类型输出格式化字符串

建议13: 为类型输出格式化字符串 有两种方法可以为类型提供格式化的字符串输出.一种是意识到类型会产生格式化字符串输出,于是让类型继承接口IFormattable.这对类型来 说,是一种主动实现的方式,要求开发者可以预见类型在格式化方面的要求.更多的时候,类型的使用者需为类型自定义格式化器,这就是第二种方法,也是最灵活 多变的方法,可以根据需求的变化为类型提供多个格式化器.下面就来详细介绍这两种方法. 最简单的字符串输出是为类型重写ToString方法,如果没有为类型重写该方法,默认会调用Obj

编写高质量代码改善C#程序的157个建议——建议90:不要为抽象类提供公开的构造方法

建议90:不要为抽象类提供公开的构造方法 首先,抽象类可以有构造方法.即使没有为抽象类指定构造方法,编译器也会为我们生成一个默认的protected的构造方法.下面是一个标准的最简单的抽象类: abstract class MyAbstractClass { protected MyAbstractClass(){} } 其次,抽象类的方法不应该是public或internal的.抽象类设计的本意是让子类继承,而不是用于生成实例对象的.如果抽象类是public或internal的,它对于其它类型

编写高质量代码改善C#程序的157个建议——建议85:Task中的异常处理

建议85:Task中的异常处理 在任何时候,异常处理都是非常重要的一个环节.多线程与并行编程中尤其是这样.如果不处理这些后台任务中的异常,应用程序将会莫名其妙的退出.处理那些不是主线程(如果是窗体程序,那就是UI主线程)产生的异常,最终的办法都是将其包装到主线程上. 在任务并行库中,如果对任务运行Wait.WaitAny.WaitAll等方法,或者求Result属性,都能捕获到AggregateException异常.可以将AggregateException异常看做是任务并行库编程中最上层的异

编写高质量代码改善C#程序的157个建议——建议89:在并行方法体中谨慎使用锁

建议89:在并行方法体中谨慎使用锁 除了建议88所提到的场合,要谨慎使用并行的情况还包括:某些本身就需要同步运行的场合,或者需要较长时间锁定共享资源的场合. 在对整型数据进行同步操作时,可以使用静态类Interlocked的Add方法,这就极大地避免了由于进行原子操作长时间锁定某个共享资源所带来的同步性能损耗.回顾建议83中的例子. static void Main(string[] args) { int[] nums = new int[] { 1, 2, 3, 4 }; int total

编写高质量代码改善C#程序的157个建议——建议87:区分WPF和WinForm的线程模型

建议87:区分WPF和WinForm的线程模型 WPF和WinForm窗体应用程序都有一个要求,那就是UI元素(如Button.TextBox等)必须由创建它的那个线程进行更新.WinForm在这方面的限制并不是很严格,所以像下面这样的代码,在WinForm中大部分情况下还能运行(本建议后面会详细解释为什么会出现这种现象): private void buttonStartAsync_Click(object sender, EventArgs e) { Task t = new Task(()