zoukankan      html  css  js  c++  java
  • C# Task和async/await详解

    一、什么是异步

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

      异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率。net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的

    异步方法基本都用到了Task;NET5.0推出了async/await,让异步编程更为方便。本篇主要介绍Task、async/await相关的内容

    二、Task介绍

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

    using System;
    using System.Threading;
    
    namespace ThreadPoolDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                for (int i = 1; i <= 10; i++)
                {
                    ThreadPool.QueueUserWorkItem(new WaitCallback((obj) =>
                    {
                        Console.WriteLine($"第{obj}个执行任务");
                    }),i);
                }
                Console.ReadKey();
            }
        }
    }
    View Code

      执行结果:

        ThreadPool相对于Thread来说可以减少线程的创建,有效减小系统开销;但是ThreadPool不能控制线程的执行顺序,我们也不能获取线程池内线程取消/异常/完成的通知,即我们不能有效监控和控制线程池中的线程。

    1、Task创建和运行

      我们知道了ThreadPool的弊端:我们不能控制线程池中线程的执行顺序,也不能获取线程池内线程取消/异常/完成的通知。net4.0在ThreadPool的基础上推出了Task,Task拥有线程池的优点,同时也解决了使用线程池不易控制的弊端。首先看一下怎么去创建并运行一个Task,Task的创建和执行方式有如下三种:

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace TaskDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                #region 方式一:NEW实例化一个Task,通过Start方法启动
                Task task = new Task(() =>
                {
                    Thread.Sleep(100);
                    Console.WriteLine($"NEW实例化一个task,线程ID为{Thread.CurrentThread.ManagedThreadId}");
                });
                task.Start();
                #endregion
    
                #region 方式二:Task.Factory.StartNew(Action action)创建和启动一个Task           
                Task task2 = Task.Factory.StartNew(() =>
                {
                    Thread.Sleep(100);
                    Console.WriteLine($"Task.Factory.StartNew方式创建一个task,线程ID为{ Thread.CurrentThread.ManagedThreadId}");
                });
                #endregion
    
                #region 方式三:Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task
                Task task3 = Task.Run(() =>
                {
                    Thread.Sleep(100);
                    Console.WriteLine($"Task.Run方式创建一个task,线程ID为{ Thread.CurrentThread.ManagedThreadId}");
                });
                #endregion
                Console.WriteLine("执行主线程!");
                Console.ReadKey();
            }
        }
    }
    View Code
     执行结果如下:

      

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

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace TaskParamDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                #region 方式一:NEW实例化一个Task,通过Start方法启动
                Task<string> task = new Task<string>(() =>
                {
                    return $"NEW实例化一个task,线程ID为{Thread.CurrentThread.ManagedThreadId}";
                });
                task.Start();
                #endregion
    
                #region 方式二:Task.Factory.StartNew(Action action)创建和启动一个Task
                Task<string> task2 = Task.Factory.StartNew<string>(() =>
                {
                    return $"Task.Factory.StartNew方式创建一个task,线程ID为{ Thread.CurrentThread.ManagedThreadId}";
                });
                #endregion
                #region 方式三:Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task
                Task<string> task3 = Task.Run<string>(() =>
                {
                    return $"Task.Run方式创建一个task,线程ID为{ Thread.CurrentThread.ManagedThreadId}";
                });
                #endregion
                Console.WriteLine("执行主线程!");
                Console.WriteLine(task.Result);
                Console.WriteLine(task2.Result);
                Console.WriteLine(task3.Result);
                Console.ReadKey();
            }
        }
    }
    View Code

      注意task.Resut获取结果时会阻塞线程,即如果task没有执行完成,会等待task执行完成获取到Result,然后再执行后边的代码,程序运行结果如下:

      

       上边的所有例子中Task的执行都是异步的,不会阻塞主线程。有些场景下我们想让Task同步执行怎么办呢?Task提供了 task.RunSynchronously()用于同步执行Task任务,代码如下:

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace TaskDemo1
    {
        class Program
        {
            static void Main(string[] args)
            {
                Task task = new Task(() =>
                {
                    Thread.Sleep(100);
                    Console.WriteLine("执行Task结束!");
                });           
                //同步执行,task会阻塞主线程
                task.RunSynchronously();
                Console.WriteLine("执行主线程结束!");
                Console.ReadKey();
            }
        }
    }
    View Code

      执行结果如下:

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

    (1)Thread阻塞线程的方法

      使用Thread时,我们知道用thread.Join()方法即可阻塞主线程。看一个例子:

    using System;
    using System.Threading;
    
    namespace TaskDemo1
    {
        class Program
        {
            static void Main(string[] args)
            {
                Thread th1 = new Thread(() => {
                    Thread.Sleep(500);
                    Console.WriteLine("线程1执行完毕!");
                });
                th1.Start();
                Thread th2 = new Thread(() => {
                    Thread.Sleep(1000);
                    Console.WriteLine("线程2执行完毕!");
                });
                th2.Start();
                //阻塞主线程
                th1.Join();
                th2.Join();
                Console.WriteLine("主线程执行完毕!");
                Console.ReadKey();
            }
        }
    }
    View Code

      执行结果:

      

      Thread的Join方法可以阻塞调用线程,但是有一些弊端:

      ①如果我们要实现很多线程的阻塞时,每个线程都要调用一次Join方法;

      ②如果我们想让所有的线程执行完毕(或者任一线程执行完毕)时,立即解除阻塞,使用Join方法不容易实现。

    (2)Task提供了 Wait/WaitAny/WaitAll 方法,可以更方便地控制线程阻塞。

      task.Wait() 表示等待task执行完毕,功能类似于thead.Join(); 

      Task.WaitAll(Task[] tasks) 表示只有所有的task都执行完成了再解除阻塞;

      Task.WaitAny(Task[] tasks)表示只要有一个task执行完毕就解除阻塞,看一个例子:

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace TaskDemo1
    {
        class Program
        {
            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();
            }
        }
    }
    View Code

      执行结果:

      

       如果将例子中的WaitAll换成WaitAny,那么任一task执行完毕就会解除线程阻塞,执行结果是:先打印【线程1执行完毕】,然后打印【主线程执行完毕】,最后打印【线程2执行完毕】

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

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

      task.WhenAll(Task[] tasks) 表示所有的task都执行完毕后再去执行后续的操作

      task.WhenAny(Task[] tasks) 表示任一task执行完毕后就开始执行后续操作。看一个例子:

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace TaskDemo1
    {
        class Program
        {
            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();
            }
        }
    }
    View Code

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

      上边的例子也可以通过 Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction)和 Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 来实现 ,修改上边例子代码如下,执行结果不变。

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace TaskDemo1
    {
        class Program
        {
            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();
            }
        }
    }
    View Code

      执行结果如下:

      

    4 Task的任务取消(CancellationTokenSource)

    (1)Thread取消任务执行

      在Task前我们执行任务采用的是Thread,Thread怎么取消任务呢?一般流程是:设置一个变量来控制任务是否停止,如设置一个变量isStop,然后线程轮询查看isStop,如果isStop为true就停止,代码如下:

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace TaskDemo1
    {
        class Program
        {
            static void Main(string[] args)
            {
                bool isStop = false;
                int index = 0;
                //开启一个线程执行任务
                Thread th1 = new Thread(() =>
                {
                    while (!isStop)
                    {
                        Thread.Sleep(1000);
                        Console.WriteLine($"第{++index}次执行,线程运行中...");
                    }
                });
                th1.Start();
                //五秒后取消任务执行
                Thread.Sleep(5000);
                isStop = true;
                Console.ReadKey();
            }
        }
    }
    View Code

      执行结果:

      (2) Task取消任务执行

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

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace TaskDemo1
    {
        class Program
        {
            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();
            }
        }
    }
    View Code

      执行结果:

      

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

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace TaskDemo1
    {
        class Program
        {
            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();
            }
        }
    }
    View Code
     执行结果如下,第5次执行在取消回调后打印,这是因为,执行取消的时候第5次任务已经通过了while()判断,任务已经执行中了:

      

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

    public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private void mySetValueBtn_Click(object sender, EventArgs e)
            {
                Task.Run(() =>
                {
                    Action<int> setValue = (i) => { myTxtbox.Text = i.ToString(); };
                    for (int i = 0; i < 1000000; i++)
                    {
                        myTxtbox.Invoke(setValue,i);
                    }
                });
            }
        }
    View Code

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

    在这里插入图片描述

    三、异步方法(async/await)

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

    using System;
    using System.IO;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace AsyncDemo
    {
        class Program
        {
            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;
            }
        }
    }
    View Code

      执行结果为:

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

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

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

    using System;
    using System.IO;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace AsyncDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine($"主程序执行开始:{DateTime.Now}");
                string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result;
                Console.WriteLine($"主程序输出的结果:{content}");
                Console.WriteLine($"主程序执行结束:{DateTime.Now}");
                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($"开始读取文件{DateTime.Now}");
                int len = await fs.ReadAsync(bytes, 0, bytes.Length);
                Console.WriteLine($"完成文件读取:{DateTime.Now}");
                string result = Encoding.UTF8.GetString(bytes);
                return result;
            }
        }
    }
    View Code

      执行结果:

       从上述可以看出,主程序调用异步方法GetContentAsync后,主程序并没有继续往下执行,而是等待GetContentAsync执行完,返回结果后才继续执行。如果调用方法要从调用中获取一个T类型的值,异步方法的返回类型就必须是Task<T>,而且调用者会等待结果返回才会继续往下执行。 

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

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

    using System;
    using System.IO;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace AsyncDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine($"主程序执行开始:{DateTime.Now}");
                GetContentAsync(Environment.CurrentDirectory + @"/test.txt");           
                Console.WriteLine($"主程序执行结束:{DateTime.Now}");
                Console.ReadKey();
            }
            //异步读取文件内容
            async static void GetContentAsync(string filename)
            {
                FileStream fs = new FileStream(filename, FileMode.Open);
                var bytes = new byte[fs.Length];
                //ReadAync方法异步读取内容,不阻塞线程
                Console.WriteLine($"开始读取文件{DateTime.Now}");
                int len = await fs.ReadAsync(bytes, 0, bytes.Length);
                Console.WriteLine($"完成文件读取:{DateTime.Now}");
            }
        }
    }
    View Code

       从上述看出,主程序调用异步方法GetContentAsync后,主程序继续往下执行。如果调用方法仅仅只是调用一下异步方法,不和异步方法做其他交互,我们可以设置异步方法签名的返回值为void,而且调用者不会等待,而是继续执行。 

     

    四、小结

      通过上边的介绍,我们知道async/await是基于Task的,而Task是对ThreadPool的封装改进,主要是为了更有效的控制线程池中的线程(ThreadPool中的线程,我们很难通过代码控制其执行顺序,任务延续和取消等等);ThreadPool基于Thread的,主要目的是减少Thread创建数量和管理Thread的成本。async/await Task是C#中更先进的,也是微软大力推广的特性,我们在开发中可以尝试使用Task来替代Thread/ThreadPool,处理本地IO和网络IO任务是尽量使用async/await来提高任务执行效率。

    参考:https://blog.csdn.net/btfireknight/article/details/97766193

  • 相关阅读:
    第二次实验课总结
    第一次实验课总结
    文件操作
    事件监听 计算器界面
    个人信息
    学生成绩管理
    银行管理
    类的抽象与封装
    求最大公约数和最小公倍数
    阶乘
  • 原文地址:https://www.cnblogs.com/qtiger/p/13497807.html
Copyright © 2011-2022 走看看