C# 异步编程之 Task 的使用


(说明:随笔内容为学习task的笔记,资料来源:https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?redirectedfrom=MSDN&view=netframework-4.7.2,下面内容的图片大多来自msdn,不是的会说明)

一、什么是task?

Task 是一个独立的操作线程,通常是异步执行的。通过Task启动的异步操作线程是在线程池中执行的,也即Task使用的是线程池的线程。

测试一下Task使用线程池的证明:

代码如下:

        public static void ThreadPoolTest()
        {
            Action action1 = () => { Console.WriteLine("I am Action, I don't need anything"); };
            for (int i = 0; i < 100; i++)
            {
                new Task(action1).Start();
            }
        }

        public static void ThreadPoolTest2()
        {
            Action action1 = () => { Console.WriteLine("I am Action, I don't need anything"); };
            for (int i = 0; i < 1000; i++)
            {
                new Thread(new ThreadStart(Print)).Start();
            }
        }

        private static void Print()
        {
            Console.WriteLine("I am Action, I don't need anything");
        }

运行结果:

这个是使用Windows Performence Monitor(性能监视器)监视的系统当前的线程数量情况,绿色的线代表的是当前应用程序托管的线程,包括正在运行的和已经停止的。图中绿色的线急剧上升的时候是我开始运行ThreadPoolTest2方法的时候(下降是我关闭程序的时候),而在后面当线程恢复平稳我运行ThreadPoolTest方法的时候线程的数量却没有什么变化。

 二、Task的构造函数:

我们主要看以下几个:
1、Task(Action):

Action是没有返回值的委托。

简单来说,直接贴代码,这个是很简单的一个操作,也很常用:

        public static void ActionParameter()
        {
            Action action1 = () => { Console.WriteLine("I am Action, I don't need anything"); };
            Action<object> action2 = (o) => { Console.WriteLine("I am Action, My Name is:"+o); };
            Task task1 = new Task(action1);
            Task task2 = new Task(action2,"a");
            Task task3 = new Task(action2, "b");

            task1.Start();
            task2.Start();
            task3.Start();
        }

2、Task(Action, CancellationToken) 

先上例子:

 CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
            var token = cancellationTokenSource.Token;
            List<int> intList = new List<int>();
            for (int i = 0; i < 1000; i++)
            {
                intList.Add(i);
            }

            Task t = Task.Run(() => {

                Parallel.ForEach(intList, 
                    i => {
                        Console.WriteLine(i);
                        Interlocked.Increment(ref num);
                        Console.WriteLine("num is:" + num);
                        if (token.IsCancellationRequested)
                            token.ThrowIfCancellationRequested();

                    }
                );
            }, token);

            Thread.Sleep(5);
            cancellationTokenSource.Cancel();
            try
            {
                await t;
            }
            catch (AggregateException aggregateExceptions)
            {
                foreach (var ex in aggregateExceptions.InnerExceptions)
                {
                    Console.WriteLine(ex.GetType().Name + "========================================");
                }
            }
            finally
            {
                cancellationTokenSource.Dispose();
            }

运行结果:

250
0
num is:2
500
num is:3
num is:1
750
num is:4
OperationCanceledException========================================
OperationCanceledException========================================
OperationCanceledException========================================
OperationCanceledException========================================

通过上面例子可以看到:
1、进行取消用的是 CancellationTokenSource (官方文档定义:向应该被取消的 System.Threading.CancellationToken 发送信号。)的属性Token(类型是CancellationToken);

2、我们在子线程中是用Parallel(用于并行编程,同时启用多个线程运行),在Parallel中当检查到当前线程已经取消都抛出了异常,但是我们try catch这些异常的地方是在await,这个是很重要的一点,而且,同时启动多个线程,异常是放在AggregateException 中;

3、我们有一行代码Thread.Sleep(5),让主线程睡眠了5毫秒,这个是为了让子线程能够获得时间片。如果不这么做很大的可能会使await等待的是一个已经取消的异步操作,会抛出TaskCanceledException异常,需要进行捕获。有兴趣可以尝试。

4、记得释放CancellationTokenSource 

也可以在内部取消线程(仅展示修改的代码块):

            Task t = Task.Run(() => {

                Parallel.ForEach(intList,
                    i => {
                        Console.WriteLine(i);
                        Interlocked.Increment(ref num);
                        if (token.IsCancellationRequested)
                            token.ThrowIfCancellationRequested();
                        if (num > 30)
                        {
                            cancellationTokenSource.Cancel();
                        }
                        Console.WriteLine("num is:" + num);
                    }
                );
            }, token);

            try
            {
                await t;
            }

 3、Task(Action, TaskCreationOptions)

Action我们已经了解了,主要看的是TaskCreationOptions,我们知道,这种命名的一般是枚举类型,看一下有什么枚举值,官网有解释:

3.1 AttachedToParent  

看一下区别:

看示例代码:

 public static async void CreateOptionTest()
        {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            Task task = new Task(() =>
            {

                Task childTask = new Task(() =>
                {
                    try
                    {
                        Thread.Sleep(1000);
                        Console.WriteLine("child task is completed--------------------------");
                        throw new ChildIsCompletedException("just need a exception");
                    }
                    catch (ChildIsCompletedException ex)
                    {
                        throw ex;
                    }
                }
                //,TaskCreationOptions.AttachedToParent
                );
                childTask.Start();

                Thread.Sleep(200);
                Console.WriteLine("parent task is completed============================");
            }, TaskCreationOptions.None);

            task.Start();
            try
            {
                await task;
                Console.WriteLine("parent task status:"+task.IsCompleted);
            }catch(AggregateException aggreEx)
            {
                IReadOnlyCollection exs = aggreEx.InnerExceptions;
                foreach (var item in exs)
                {
                    Console.WriteLine(item.GetType().Name);
                }
            }
        }

有添加AttachedToParent的运行结果:

parent task is completed============================
child task is completed--------------------------
ChildIsCompletedException

没有添加的结果:

parent task is completed============================
parent task status:True
child task is completed--------------------------

没有添加到父任务的时候是没有抛出异常的,而且父任务完成了但是它还没有退出,需要等待子任务完成才算完成,然而子任务抛出了异常,所以它就挂了,抛出了子任务的异常,而不附加到父任务的时候,就没有异常抛出,父任务正常完成了。

 3.2  DenyChildAttach 使父任务拒绝任何尝试的子任务附加;

在3.1的例子中,我们父任务的TaskCreationOptions的值是None,改成DenyChildAttach,即使子任务使用了AttachedToParent,输出结果也会是像没有添加一样。

 

4、 属性:

 4.1 AsyncState:

AsyncState为传入线程的参数,类型是Object,以委托Action创建线程为例,委托Action可以传入一个参数,AsyncState就是这个参数,请见:

        public static void GetAsyncStateTest()
        {
            Func<int> func = () => 1*2;
            Task task = new Task((x) => { Console.WriteLine("I am Action"); }, "I am AsyncState");
            task.Start();
            Console.WriteLine(task.AsyncState);
        }

运行结果:

I am AsyncState
I am Action

4.2 CompletedTask  (与FromResult(T object)相比)

CompletedTask和FromResult都返回一个状态为RanToCompletion的Task,区别是一个有返回值一个没有。当我们允许一个线程需要返回一个task的时候,如果在早期就已经知道了结果线程不需要继续往下运行可以根据需不需要返回值使用这两个中的一个。

public static void GetCompletedTask()
        {
            Funcstring>> funcTask = () =>
             {
                 for (int i = 0; i < 1000; i++)
                 {
                     if (i > 100)
                     {
                         return Task.FromResult<string>("已完成");
                     }
                 }

                 return new Task<string>((x) => { return (string)x; }, "全部完成");
             };

            Func funcTaskNotResult = () =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    if (i > 100)
                    {
                        return Task.CompletedTask;
                    }
                }

                return new Task<string>((x) => { return (string)x; }, "全部完成");
            };

            Task<string> task=Task.Run<string>(funcTask);
            Task taskNotResult = Task.Run(funcTaskNotResult);
            Thread.Sleep(1000);
            Console.WriteLine("task result:");

            Console.WriteLine("status:"+task.Status);
            Console.WriteLine("is completed:"+task.IsCompleted);
            Console.WriteLine("result"+task.Result);
            Console.WriteLine("taskNotResult result:");
            Console.WriteLine("status:" + taskNotResult.Status);
            Console.WriteLine(taskNotResult.IsCompleted);
        }

返回结果:

task result:
status:RanToCompletion
is completed:True
result已完成
taskNotResult result:
status:RanToCompletion
True

 持续更新中 ing