c# Thread、ThreadPool、Task有什么区别,什么时候用,以及Task的使用

c# Thread、ThreadPool、Task有什么区别,什么时候用,以及Task的使用

这三者都是为了处理耗时任务,且都是异步的。

Thread

Thread就是Thread,需要自己调度,适合长跑型的操作。

ThreadPoll

ThreadPool是Thread基础上的一个线程池,目的是减少频繁创建线程的开销。线程很贵,要开新的stack,要增加CPU上下文切换,所以ThreadPool适合频繁、短期执行的小操作。调度算法是自适应的,会根据程序执行的模式调整配置,通常不需要自己调度线程。另外分为Worker和IO两个池。IO线程对应Native的overlapped io,Win下利用IO完成端口实现非阻塞IO。

【Thread vs. ThreadPoll】

前台线程:主程序必须等待线程执行完毕后才可退出程序。Thread默认为前台线程,也可以设置为后台线程。

后台线程:主程序执行完毕后就退出,不管线程是否执行完毕。ThreadPool默认为后台线程。

线程消耗:开启一个新线程,线程不做任何操作,都要消耗1M左右的内存。

总结:ThreadPoll 性能优于 Thread,但是 Thread 和 ThreadPoll 对线程的控制都不是很好,例如线程等待(线程执行一段时间无响应后,直接停止线程),释放资源等,都没有直接的API来控制,只能通过硬编码来实现。同时,ThreadPool 使用的是线程池全局队列,全局队列中的线程依旧会存在竞争共享资源的情况,从而影响性能。

task

Task 是在 .NET Framework 4 中添加进来的。这是新的 namespace:System.Threading.Tasks;它强调的是 adding parallelism and concurrency to applications。在语法上,和 lamda 表达式更好地结合。

Task 背后的实现也是使用了线程池线程,但它的性能优于ThreadPoll,因为它使用的不是线程池的全局队列,而是本地队列,使线程之间的资源竞争减少。同时,Task 提供了丰富的 API 来管理、控制线程。但是相对前面两种耗内存,Task 依赖于 CPU,对于多核的CPU性能远超前两者,而对於单核的CPU,三者性能没什么差别。

创建Task有两种方法:

//工厂创建,直接执行
Task t = Task.Factory.StartNew(() => {

                Console.WriteLine("任务已启动....");

            });

或者

//直接实例化,必须手动去Start
Task t2 = new Task(() => {

                Console.WriteLine("开启一个新任务");

            });

            t2.Start();//任务已启动...

第一种方法不需要调用start,初始化后任务就开始了。

Task还可以随时取消正在执行的任务,请看下面的代码

static void Main(string[] args) {
        CancellationTokenSource cts = new CancellationTokenSource();            
        Task t3 = new Task(() => LongRunTask(cts.Token));            
        t2.Start();            
        Thread.Sleep(3000);            
        cts.Cancel();            
        Console.Read();
}

private static void LongRunTask(CancellationToken token) {            
    while (true) {                
        if (!token.IsCancellationRequested) {                    
            Thread.Sleep(500);                    
            Console.WriteLine(".");                
        } else {                    
            Console.WriteLine("任务取消了");                    
            break;                
        }            
    }        
}

Task的任务控制:Task最吸引人的地方就是他的任务控制了,你可以很好的控制task的执行顺序,让多个task有序的工作

方法名 说明
Task.Wait task1.Wait(); 就是等待任务执行(task1)完成,task1的状态变为Completed
Task.WaitAll 待所有的任务都执行完成
Task.WaitAny 同Task.WaitAll,就是等待任何一个任务完成就继续向下执行
Task.ContinueWith 第一个Task完成后自动启动下一个Task,实现Task的延续
CancellationTokenSource 通过cancellation的tokens来取消一个Task

使用 Task 代替 ThreadPool 和 Thread

【Task 的优势】

ThreadPool 相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便。比如:

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

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

以往,如果开发者要实现上述功能,需要完成很多额外的工作,现在,FCL中提供了一个功能更强大的概念:Task。Task在线程池的基础上进行了优化,并提供了更多的API。在 FCL4.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的完成状态】

任务 Task 有这样一些属性,让我们查询任务完成时的状态:

1: IsCanceled,因为被取消而完成;

2: IsCompleted,成功完成;

3: 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方法进行处理。如果采用后者的方式,如下:

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中,如果任务是通过 ThrowIfCancellationRequested 方法结束的,对任务求结果值将会抛出异常 OperationCanceledException,而不是得到抛出异常前的结果值。这意味着任务是通过异常的方式被取消掉的,所以可以注意到上面代码的输出中,状态 IsCancled 为True。

再一次,我们注意到取消是通过异常的方式实现的,而表示任务中发生了异常的IsFaulted状态却还是等于False。这是因为 ThrowIfCancellationRequested 是协作式取消方式类型 CancellationTokenSource 的一个方法,CLR进行了特殊的处理。CLR知道这一行程序开发者有意为之的代码,所以不把它看作是一个异常(它被理解为取消)。要得到 IsFaulted 等于 True 的状态,我们可以修改 While 循环,模拟一个异常出来:

while (true)
            {
                //ct.ThrowIfCancellationRequested();
                if (result == 5)
                {
                    thrownew 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甚至进一步优化了后台线程池的调度,加快了线程的处理速度。在FCL4.0时代,使用多线程,我们理应更多地使用Task。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章