c#_Task用法总结

目录

Task与Thread

Task的用法

1、创建任务

2、async/await的实现方式 

3、task可以同步执行吗?

4、Task的Wait、WaitAny、WaitAll方法介绍

5、释放、取消Task

6、Task的Wait、WaitAny、WaitAll方法介绍


 

Task与Thread

  • Task是架构在Thread之上的,也就是说任务最终还是要抛给线程去执行。
  • Task跟Thread不是一对一的关系,比如开10个任务并不是说会开10个线程,这一点任务有点类似线程池,但是任务相比线程池有很小的开销和精确的控制。

ThreadPool相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便。比如:
  ◆ ThreadPool不支持线程的取消、完成、失败通知等交互性操作;
  ◆ ThreadPool不支持线程执行的先后次序;

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

net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;net5.0推出了async/await,让异步编程更为方便。

Task的用法

1、创建任务

无返回值的方式

方式1 :  var t1 = new Task(() => TaskMethod("Task 1"));
  t1.Start();
  Task.WaitAll(t1);//等待所有任务结束

  
  注 :  任务的状态:
  Start之前为:Created
  Start之后为:WaitingToRun

方式2 :  Task.Run(() => TaskMethod("Task 2"));

方式3 :  Task.Factory.StartNew(() => TaskMethod("Task 3")); 直接异步的方法

       或者 :
  var t3=Task.Factory.StartNew(() => TaskMethod("Task 3"));
  Task.WaitAll(t3);//等待所有任务结束
  注 :
  任务的状态:
  Start之前为:Running
  Start之后为:Running

using System;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var t1 = new Task(() => TaskMethod("Task 1"));
            var t2 = new Task(() => TaskMethod("Task 2"));
            t2.Start();
            t1.Start();
            Task.WaitAll(t1, t2);
            Task.Run(() => TaskMethod("Task 3"));
            Task.Factory.StartNew(() => TaskMethod("Task 4"));
            //标记为长时间运行任务,则任务不会使用线程池,而在单独的线程中运行。
            Task.Factory.StartNew(() => TaskMethod("Task 5"), TaskCreationOptions.LongRunning);
 
            #region 常规的使用方式
            Console.WriteLine("主线程执行业务处理.");
            //创建任务
            Task task = new Task(() =>
            {
                Console.WriteLine("使用System.Threading.Tasks.Task执行异步操作.");
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine(i);
                }
            });
            //启动任务,并安排到当前任务队列线程中执行任务(System.Threading.Tasks.TaskScheduler)
            task.Start();
            Console.WriteLine("主线程执行其他处理");
            task.Wait();
            #endregion
 
            Thread.Sleep(TimeSpan.FromSeconds(1));
            Console.ReadLine();
        }
 
        static void TaskMethod(string name)
        {
            Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
                name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
        }
    }
}

带返回值的方式

方式4 :  

       Task task = CreateTask("Task 1");
  task.Start();
  int result = task.Result;

using System;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApp1
{
    class Program
    {
        static Task CreateTask(string name)
        {
            return new Task(() => TaskMethod(name));
        }
 
        static void Main(string[] args)
        {
            TaskMethod("Main Thread Task");
            Task task = CreateTask("Task 1");
            task.Start();
            int result = task.Result;
            Console.WriteLine("Task 1 Result is: {0}", result);
 
            task = CreateTask("Task 2");
            //该任务会运行在主线程中
            task.RunSynchronously();
            result = task.Result;
            Console.WriteLine("Task 2 Result is: {0}", result);
 
            task = CreateTask("Task 3");
            Console.WriteLine(task.Status);
            task.Start();
 
            while (!task.IsCompleted)
            {
                Console.WriteLine(task.Status);
                Thread.Sleep(TimeSpan.FromSeconds(0.5));
            }
 
            Console.WriteLine(task.Status);
            result = task.Result;
            Console.WriteLine("Task 3 Result is: {0}", result);
 
            #region 常规使用方式
            //创建任务
            Task getsumtask = new Task(() => Getsum());
            //启动任务,并安排到当前任务队列线程中执行任务(System.Threading.Tasks.TaskScheduler)
            getsumtask.Start();
            Console.WriteLine("主线程执行其他处理");
            //等待任务的完成执行过程。
            getsumtask.Wait();
            //获得任务的执行结果
            Console.WriteLine("任务执行结果:{0}", getsumtask.Result.ToString());
            #endregion
        }
 
        static int TaskMethod(string name)
        {
            Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
                name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
            Thread.Sleep(TimeSpan.FromSeconds(2));
            return 42;
        }
 
        static int Getsum()
        {
            int sum = 0;
            Console.WriteLine("使用Task执行异步操作.");
            for (int i = 0; i < 100; i++)
            {
                sum += i;
            }
            return sum;
        }
    }
}

2、async/await的实现方式 

(异步方法:在执行完成前立即返回调用方法,在调用方法继续执行的过程中完成任务。)

c#_Task用法总结_第1张图片

c#_Task用法总结_第2张图片

       ①从 Main 方法执行到 CountCharactersAsync(1, url1) 方法时,该方法会立即返回然后才会调用它内部的方法开始下载内容。该方法返回的是一个 Task 类型的占位符对象,表示计划进行的工作。这个占位符最终会返回 int 类型的值。

  ②这样就可以不必等 CountCharactersAsync(1, url1) 方法执行完成就可以继续进行下一步操作。到执行 CountCharactersAsync(2, url2)  方法时,跟 ① 一样返回 Task 对象。

  ③然后,Main 方法继续执行三次 ExtraOperation 方法,同时两次 CountCharactersAsync 方法依然在持续工作 。

  ④t1.Result 和 t2.Result 是指从 CountCharactersAsync 方法调用的 Task 对象取结果,如果还没有结果的话,将阻塞,直有结果返回为止。

     async/await 结构可分成三部分:

  1. 调用方法:该方法调用异步方法,然后在异步方法执行其任务的时候继续执行;
  2. 异步方法:该方法异步执行工作,然后立刻返回到调用方法;
  3. await 表达式:用于异步方法内部,指出需要异步执行的任务。一个异步方法可以包含多个 await 表达式(不存在 await 表达式的话 IDE 会发出警告)。
  • 注意await异步等待的地方,await后面的代码和前面的代码执行的线程可能不一样
  • async关键字创建了一个状态机;await会解除当前线程的阻塞,完成其他任务

c#_Task用法总结_第3张图片

     语法分析:

  1. 关键字:方法头使用 async 修饰。
  2. 要求:包含 N(N>0)await 表达式(不存在 await 表达式的话 IDE 会发出警告),表示需要异步执行的任务。
  3. 返回类型:只能返回 3 种类型(void、Task 和 Task。Task 和 Task 标识返回的对象会在将来完成工作,表示调用方法和异步方法可以继续执行。
  4. 参数:数量不限但不能使用 out 和 ref 关键字
  5. 命名约定:方法后缀名应以 Async 结尾
  6. 其它:匿名方法和 Lambda 表达式也可以作为异步对象;async 是一个上下文关键字;关键字 async 必须在返回类型前。

c#_Task用法总结_第4张图片


3、task可以同步执行吗?

通过上面的实际代码测试,我们知道task都是异步执行,那么有人会问,task可以实现同步执行吗?不急,强大的微软也想到了这个问题,于是乎,提供了

1)task.RunSynchronously()

来同步执行,但是这种方式执行,只有通过new 实例化的task才有效,原因也很简单,其他两种方式创建task都已经自启动执行了,不可能在来一个同步启动执行吧,嘿嘿。下面我们用代码来演示:

/// 
 /// 通过RunSynchronously 实现task的同步执行
 /// 
 private static void TaskRunSynchronously()
 {
     Console.WriteLine("主线程开始执行!");

     Task task = new Task(() =>
     {
         Thread.Sleep(100);
         Console.WriteLine("Task执行结束!");
         return "";
     });

     /// task.Start();
     /// task.Wait();

     // 获取执行结果,会阻塞主流程
     // string result = task.Result;

     //同步执行,task会阻塞主线程
     task.RunSynchronously();

     Console.WriteLine("执行主线程结束!");
     Console.ReadKey();
 }

执行结果:很明显主线程阻塞等待task同步执行。

在这里插入图片描述

2)task同步执行,出了上面的实现方式,其实我们也可以通过task.wait()来变相的实现同步执行效果,当然也可以用task.Result来变现的实现,原理很简单,因为wait()和Result都是要阻塞主流程,直到task执行完毕,是不是有异曲同工之妙呢!以代码为例:
通过task.wait()实现,只需要对上面的代码做一个简单的调整,如下:其最终的效果一样:

/// 
 /// 通过RunSynchronously 实现task的同步执行
 /// 
 private static void TaskRunSynchronously()
 {
     Console.WriteLine("主线程开始执行!");

     Task task = new Task(() =>
     {
         Thread.Sleep(100);
         Console.WriteLine("Task执行结束!");
         return "";
     });

      task.Start();
      task.Wait();

     // 获取执行结果,会阻塞主流程
     // string result = task.Result;

     // 同步执行,task会阻塞主线程
     // task.RunSynchronously();

     Console.WriteLine("执行主线程结束!");
     Console.ReadKey();
 }

执行结果:
在这里插入图片描述

3)通过task.Result 实现,前提是task一定要有返回值,如下:其最终的效果一样:

/// 
 /// 通过RunSynchronously 实现task的同步执行
 /// 
 private static void TaskRunSynchronously()
 {
     Console.WriteLine("主线程开始执行!");

     Task task = new Task(() =>
     {
         Thread.Sleep(100);
         Console.WriteLine("Task执行结束!");
         return "";
     });

      task.Start();
     /// task.Wait();

     // 获取执行结果,会阻塞主流程
     string result = task.Result;

     // 同步执行,task会阻塞主线程
     // task.RunSynchronously();

     Console.WriteLine("执行主线程结束!");
     Console.ReadKey();
 }

在这里插入图片描述

执行效果也和上面的两种方式一样。

当然我还可以通过task.IsCompleted来变现实现,在此就不在细说,简单一个代码示意即可:while
(!task.IsCompleted){}
当然我上面说的几种实现同步的方式,只是为了拓展一下思路,不一定都是最优方案。


4、Task的Wait、WaitAny、WaitAll方法介绍

task的基本创建和用法,上面都做了简单的介绍,但是在我们实际业务场景中,往往不是那么简单的单纯实现。比如:还是刚刚上面的那个酒店信息获取为例,现在新的需求是:3个渠道的接口实时数据,我们只需要获取到其中的一个就立即返回会用户,避免用户等待太久,那么这个时候task.WaitAny就派上用场了,WaitAny就是等待一组tsak集合中,只要有一个执行完毕就不在等待,与之对应的是WaitAll需要等待一组tsak集合中所有tsak都执行完毕,当然了Wait是针对一个task的,等待本身执行完成,上面的模拟同步执行已经说了,就不在啰嗦。

/// 
 /// 获取最新的客房信息(只需要获取到一个即可)
 /// 
 /// 客房信息集合
 private static List GetOneHotelRoomInfro()
 {
     // 模拟存储获取到的酒店客房数据集合
     List listHotelRoomInfro = new List();

     Console.WriteLine("下面通过3个task,并行的到不同接口方获取实时的客房信息
     :");
     Console.WriteLine("");

     // 在此我也分别对3种不同渠道,采用3种不同的方式来实现

     // 其一、通过传统的 new 方式来实例化一个task对象,获取 携程 的客房数据
     Task newCtripTask = new Task(() =>
     {
         // 具体获取业务逻辑处理...
         Thread.Sleep(new Random().Next(100, 1000));
         listHotelRoomInfro.Add("我是来自 携程 的最新客房信息");
     });

     // 启动 tsak
     newCtripTask.Start();

     // 其二、通过工厂 factory 来生成一个task对象,并自启动:获取 艺龙
      的客房数据
     Task factoryElongTask = Task.Factory.StartNew(() =>
     {
         // 具体获取业务逻辑处理...
         Thread.Sleep(new Random().Next(100, 1000));
         listHotelRoomInfro.Add("我是来自 艺龙 的最新客房信息");
     });

     // 其三、通过 Task.Run(Action action) 来创建一个自启动task:
     获取 去哪儿网 的客房数据
     Task runQunarTask = Task.Run(() =>
     {
         // 具体获取业务逻辑处理...
         Thread.Sleep(new Random().Next(100, 1000));
         listHotelRoomInfro.Add("我是来自 去哪儿网 的最新客房信息");
     });

     // 只需要等待一个有返回即可
     Task.WaitAny(new Task[] { newCtripTask, factoryElongTask, 
     runQunarTask });

     // 等待所有接口数据返回
     // Task.WaitAll(new Task[] { newCtripTask, factoryElongTask, 
     runQunarTask });

     Console.WriteLine("已经有接口数据返回!");
     foreach (var item in listHotelRoomInfro)
     {
         Console.WriteLine($"返回的接口数据为:{item}");
     }

     Console.WriteLine("主线程执行完毕!");
     Console.ReadKey();

     Console.WriteLine("");
     return listHotelRoomInfro;
 }

c#_Task用法总结_第5张图片


5、释放、取消Task

1)Task实现了IDispose接口,而且提供了Dispose方法。这意味着我应该Dispose所有的Task吗?

    这是我简短的回答:不,不用非要Dispose你的Task。

    这是我中等长度的答案:不用。不用费心去Dispose你的Task,除非性能或者弹性测试需要你去基于使用方式去Dispose Task来达到性能目标。当你需要去Dispose 那些Task,仅在情形简单的时候去做,即当你100%确认代码中Task已经成功完成(IsCompleted为true)而且没有其他人使用这些Task。

    如果有喝咖啡并阅读的时间,可以看看下面的长答案:在高的设计层面,.Net Framework设计准备表明如果一个类型持有其他的IDispose资源,那么它应该实现IDispose接口。所以Task有Dispose方法。在内部,Task可以分配一个被用来等待Task成功完成的WaitHandle。WaitHandle实现了IDispose接口,因为它内部持有实现了IDispose的SafeWaitHandle。SafeWaitHandle包含一个本地Handle资源:如果SafeWaitHandle没有Dispose,最终它的终结器(finalizer)会清理所有的被包含的handle资源,但是与此同时它的资源不会被清理干净,这会给系统造成压力。通过Task实现IDispose接口,我们使得关心积极清理这些资源的开发者可以及时地清理这些资源。

 

带来的问题

如果每一个Task分配一个WaitHandle,出于性能考虑,积极地dispose这些task是个好主意。但事实并非如此。事实上很少的task真正地分配了WaitHandle。在.Net 4里面,WaitHandle在几种情况下被延迟加载:

  • 如果Task.IAsyncResult.AsyncWaitHandle属性(显式实现了接口)被访问;

  • 如果Task被Task.WaitAll或者Task.WaitAny调用,且Task尚未成功完成。

而且在.Net 4中,一旦Task被dispose了,它的大多数成员在被访问的时候会抛出ObjectDisposedExceptions异常。这使得缓存已经完成的task变得困难(可能因为性能原因缓存task),因为一旦一个消费者dispose了某个task,另一个消费者将不能访问这个task的重要成员,比如ContinueWith或者这个task的Result。

 

2)CancellationTokenSource用于取消基于Task建立的线程(单个线程、多个线程都可以)

如果 CacellationToken 在 Task 调度前取消, Task 会被取消,永远都不会执行。但是,如果Task 已调度,那么Task 为了允许它的操作在执行期间取消,Task 的代码就必须显示支持取消。也就是在一个已经开始执行的任务时,单单使用Cancel 函数是不起作用的,任务根本不会被取消。

CancellationTokenSource cts = new CancellationTokenSource ();
Task.Run(() =>{……} , cts.Token);//将cts.Token传入任务中,在外部通过控制cts实现对任务的控制
cts.Cancel();//传达取消请求

bool taskState = cts.IsCancellationRequested;//判断任务是否取消,放while中用于跳出循环

 CancellationTokenSource的功能不仅仅是取消任务执行,我们可以使用 source.CancelAfter(5000)实现5秒后自动取消任务,也可以通过 source.Token.Register(Action action)注册取消任务触发的回调函数,即任务被取消时注册的action会被执行。

        static void Main(string[] args)
        {
            CancellationTokenSource source = new CancellationTokenSource();
            //注册任务取消的事件
            source.Token.Register(() =>
            {
                Console.WriteLine("任务被取消后执行xx操作!");
            });

            int index = 0;
            //开启一个task执行任务
            Task task1 = new Task(() =>
              {
                  while (!source.IsCancellationRequested)
                  {
                      Thread.Sleep(1000);
                      Console.WriteLine($"第{++index}次执行,线程运行中...");
                  }
              });
            task1.Start();
            //延时取消,效果等同于Thread.Sleep(5000);source.Cancel();
            source.CancelAfter(5000);
            Console.ReadKey();
        }

 执行结果如下,第5次执行在取消回调后打印,这是因为,执行取消的时候第5次任务已经通过了while()判断,任务已经执行中了:

在这里插入图片描述


6、Task的Wait、WaitAny、WaitAll方法介绍

Wait/WaitAny/WaitAll方法返回值为void,这些方法单纯的实现阻塞线程。我们现在想让所有task执行完毕(或者任一task执行完毕)后,开始执行后续操作,怎么实现呢?

这时就可以用到WhenAny/WhenAll方法了,这些方法执行完成返回一个task实例。 task.WhenAll(Task[] tasks) 表示所有的task都执行完毕后再去执行后续的操作, task.WhenAny(Task[] tasks) 表示任一task执行完毕后就开始执行后续操作。

        static void Main(string[] args)
        {
            Task task1 = new Task(() => {
                Thread.Sleep(500);
                Console.WriteLine("线程1执行完毕!");
            });
            task1.Start();
            Task task2 = new Task(() => {
                Thread.Sleep(1000);
                Console.WriteLine("线程2执行完毕!");
            });
            task2.Start();
            //task1,task2执行完了后执行后续操作
            Task.WhenAll(task1, task2).ContinueWith((t) => {
                Thread.Sleep(100);
                Console.WriteLine("执行后续操作完毕!");
            });

            Console.WriteLine("主线程执行完毕!");
            Console.ReadKey();
        }

执行结果如下,我们看到WhenAll/WhenAny方法不会阻塞主线程,当使用WhenAll方法时所有的task都执行完毕才会执行后续操作;如果把栗子中的WhenAll替换成WhenAny,则只要有一个线程执行完毕就会开始执行后续操作,这里不再演示。

在这里插入图片描述

你可能感兴趣的:(c#_Task用法总结)