【小白专用24.6.8】C# 异步任务Task和异步方法async/await详解

一、什么是异步

同步和异步主要用于修饰方法。当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。

异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率。net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;net5.0推出了async/await,让异步编程更为方便。本篇主要介绍Task、async/await相关的内容,其他异步操作的方式会在下一篇介绍。

二、Task介绍

Task是在ThreadPool的基础上推出的,我们简单了解下ThreadPool。ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务,任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用。当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务,如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行。线程池能减少线程的创建,节省开销

1、Task创建和运行

首先看一下怎么去创建并运行一个Task,Task的创建和执行方式有如下三种:

cs 复制代码
 static void Main(string[] args)
 {

     //1.new方式实例化一个Task,需要通过Start方法启动
     Task task = new Task(() =>
     {
         Thread.Sleep(100);
         Console.WriteLine($"hello, task1的线程ID为{Thread.CurrentThread.ManagedThreadId}");
     });
     task.Start();

     //2.Task.Factory.StartNew(Action action)创建和启动一个Task
     Task task2 = Task.Factory.StartNew(() =>
     {
         Thread.Sleep(100);
         Console.WriteLine($"hello, task2的线程ID为{Thread.CurrentThread.ManagedThreadId}");
     });

     //3.Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task
     Task task3 = Task.Run(() =>
     {
         Thread.Sleep(100);
         Console.WriteLine($"hello, task3的线程ID为{Thread.CurrentThread.ManagedThreadId}");
     });
     Console.WriteLine("执行主线程!");
     Console.ReadKey();


 }

执行结果如下:

我们看到先打印"执行主线程",然后再打印各个任务,说明了Task不会阻塞主线程。上边的例子Task都没有返回值,我们也可以创建有返回值的Task,用法和没有返回值的基本一致,我们简单修改一下上边的栗子,代码如下:

cs 复制代码
        static void Main(string[] args)
        {

            //1.new方式实例化一个Task,需要通过Start方法启动
            Task<string> task = new Task<string>(() =>
            {
                return $"hello, task1的ID为{Thread.CurrentThread.ManagedThreadId}";
            });
            task.Start();

            //2.Task.Factory.StartNew(Func func)创建和启动一个Task
           Task<string> task2 = Task.Factory.StartNew<string>(() =>
           {
               return $"hello, task2的ID为{Thread.CurrentThread.ManagedThreadId}";
           });

            //3.Task.Run(Func func)将任务放在线程池队列,返回并启动一个Task
           Task<string> task3 = Task.Run<string>(() =>
           {
               return $"hello, task3的ID为{Thread.CurrentThread.ManagedThreadId}";
           });

            Console.WriteLine("执行主线程!");
            Console.WriteLine(task.Result);
            Console.WriteLine(task2.Result);
            Console.WriteLine(task3.Result);
            Console.ReadKey();

        }

注意task.Resut获取结果时会阻塞Task线程(Task的执行都是异步的,获取一个线程执行该方法内部的业务),不会阻塞主线程,即如果task没有执行完成,会等待task执行完成获取到Result,然后再执行后边的代码,程序运行结果如下:

有些场景下我们想让Task同步执行怎么办呢?Task提供了 task.RunSynchronously() 用于同步执行Task任务,代码如下:

cs 复制代码
static void Main(string[] args)
{

    Task task = new Task(() =>
    {
        Thread.Sleep(100);
        Console.WriteLine("执行Task结束!");
    });
    //同步执行,task会阻塞主线程
    task.RunSynchronously();
    Console.WriteLine("执行主线程结束!");
    Console.ReadKey();

}

执行结果如下:

2、Task的阻塞方法(Wait/WaitAll/WaitAny)

**实现阻塞主线程,**task.Wait() 表示等待task执行完毕; Task.WaitAll(Task[] tasks) 表示只有所有的task都执行完成了再解除阻塞; Task.WaitAny(Task[] tasks) 表示只要有一个task执行完毕就解除阻塞,看一个例子:

cs 复制代码
        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都执行完毕再执行主线程
            //执行【task1.Wait();task2.Wait();】可以实现相同功能
            Task.WaitAll(new Task[] { task1, task2 });
            Console.WriteLine("主线程执行完毕!");
            Console.ReadKey();

        }

执行结果如下:

如果将栗子中的WaitAll换成WaitAny,那么任一task执行完毕就会解除线程阻塞,执行结果是:

3、Task的延续操作(WhenAny/WhenAll/ContinueWith)

上边的Wait/WaitAny/WaitAll方法返回值为void,这些方法单纯的实现阻塞线程。我们现在想让所有task执行完毕(或者任一task执行完毕)后,开始执行后续操作,怎么实现呢?这时就可以用到WhenAny/WhenAll方法了,这些方法执行完成返回一个task实例。

task.WhenAll(Task[] tasks) 表示所有的task都执行完毕后再去执行后续的操作, task.WhenAny(Task[] tasks) 表示任一task执行完毕后就开始执行后续操作

cs 复制代码
 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,则只要有一个线程执行完毕就会开始执行后续操作,执行结果如下

Task.Factory.ContinueWhenAll(Task[] tasks、Action continuationAction) 和 Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 来实现 ,修改上边栗子代码如下,执行结果不变

cs 复制代码
 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();
     //通过TaskFactroy实现
     Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) =>
     {
         Thread.Sleep(100);
         Console.WriteLine("执行后续操作");
     });

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

 }

4、Task的任务取消(CancellationTokenSource)

1.Task取消任务执行

Task中有一个专门的类 CancellationTokenSource 来取消任务执行,还是使用上边的例子,我们修改代码如下,程序运行的效果不变。

cs 复制代码
static void Main(string[] args)
{

    CancellationTokenSource source = new CancellationTokenSource();
    int index = 0;
    //开启一个task执行任务
    Task task1 = new Task(() =>
    {
        while (!source.IsCancellationRequested)
        {
            Thread.Sleep(1000);
            Console.WriteLine($"第{++index}次执行,线程运行中...");
        }
    });
    task1.Start();
    //五秒后取消任务执行
    Thread.Sleep(5000);
    //source.Cancel()方法请求取消任务,IsCancellationRequested会变成true
    source.Cancel();
    Console.ReadKey();


}

执行结果如下

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

cs 复制代码
 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()判断,任务已经执行中了:

最后看上一篇跨线程的例子,点击按钮启动一个任务,给tetxtbox赋值,我们把Thread改成Task,代码如下:

cs 复制代码
private void BtnMySetValue_Click(object sender, EventArgs e)
{
    Task.Run(() =>
    {
        Action<int> setValue = (i) => { txtMy.Text = i.ToString(); };
        for (int i = 0; i < 1000000; i++)
        {
            txtMy.Invoke(setValue, i);
        }
    });

}

运行界面如下,赋值的task不会阻塞UI线程:

三、异步方法(async/await)

在C#5.0中出现的 async 和 await ,让异步编程变得更简单。我们看一个获取文件内容的例子:

cs 复制代码
        static void Main(string[] args)
        {

            string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result;
            //调用同步方法
            //string content = GetContent(Environment.CurrentDirectory + @"/test.txt");
            Console.WriteLine(content);
            Console.ReadKey();

        }


        //异步读取文件内容
        async static Task<string> GetContentAsync(string filename)
        {

            FileStream fs = new FileStream(filename, FileMode.Open);
            var bytes = new byte[fs.Length];
            //ReadAync方法异步读取内容,不阻塞线程
            Console.WriteLine("开始读取文件");
            int len = await fs.ReadAsync(bytes, 0, bytes.Length);
            string result = Encoding.UTF8.GetString(bytes);
            return result;
        }
        //同步读取文件内容
        static string GetContent(string filename)
        {
            FileStream fs = new FileStream(filename, FileMode.Open);
            var bytes = new byte[fs.Length];
            //Read方法同步读取内容,阻塞线程
            int len = fs.Read(bytes, 0, bytes.Length);
            string result = Encoding.UTF8.GetString(bytes);
            return result;
        }

test.txt内容是【正在学习中】执行结果为:

上边的栗子也写出了同步读取的方式,将main函数中的注释去掉即可同步读取文件内容。我们可以看到异步读取代码和同步读取代码基本一致。async/await让异步编码变得更简单,我们可以像写同步代码一样去写异步代码。注意一个小问题:异步方法中方法签名返回值为Task,代码中的返回值为T。上边例子中GetContentAsync的签名返回值为Task,而代码中返回值为string。牢记这一细节对我们分析异步代码很有帮助。

异步方法签名的返回值有以下三种:

① Task:如果调用方法想通过调用异步方法获取一个T类型的返回值,那么签名必须为Task;

② Task:如果调用方法不想通过异步方法获取一个值,仅仅想追踪异步方法的执行状态,那么我们可以设置异步方法签名的返回值为Task;

③ void:如果调用方法仅仅只是调用一下异步方法,不和异步方法做其他交互,我们可以设置异步方法签名的返回值为void,这种形式也叫做"调用并忘记"。

小结:到这里Task,async/await的简单使用已经基本结束了,一些高级特性等到工作遇到了再去研究。