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介绍

    2、Task的用法

      2.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

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

    复制代码
            static void Main(string[] args)
            {
                for (int i = 1; i <=10; i++)
                {
                    //ThreadPool执行任务
                    ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => {
                        Console.WriteLine($"第{obj}个执行任务");
                    }),i);
                }
                Console.ReadKey();
            }
    复制代码

      上边的代码通过ThreadPool执行了10个任务,执行结果为:

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

    1 Task创建和运行

      我们知道了ThreadPool的弊端:我们不能控制线程池中线程的执行顺序,也不能获取线程池内线程取消/异常/完成的通知。net4.0在ThreadPool的基础上推出了Task,Task拥有线程池的优点,同时也解决了使用线程池不易控制的弊端。

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

    复制代码
            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<TResult>,用法和没有返回值的基本一致,我们简单修改一下上边的栗子,代码如下:

    复制代码
            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执行完成获取到Result,然后再执行后边的代码,程序运行结果如下:   

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

    复制代码
            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)

    1 Thread阻塞线程的方法

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

    复制代码
            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();
            }
    复制代码

      如果注释掉两个Join,执行结果是:先打印【主线程执行完毕】,而添加两个Join方法后执行结果如下,实现了线程阻塞:

    2 Task的Wait/WaitAny/WaitAll方法

       Thread的Join方法可以阻塞调用线程,但是有一些弊端:①如果我们要实现很多线程的阻塞时,每个线程都要调用一次Join方法;②如果我们想让所有的线程执行完毕(或者任一线程执行完毕)时,立即解除阻塞,使用Join方法不容易实现。Task提供了 Wait/WaitAny/WaitAll  方法,可以更方便地控制线程阻塞。

      task.Wait()  表示等待task执行完毕,功能类似于thead.Join();  Task.WaitAll(Task[] tasks)  表示只有所有的task都执行完成了再解除阻塞;  Task.WaitAny(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都执行完毕再执行主线程
           //执行【task1.Wait();task2.Wait();】可以实现相同功能
                Task.WaitAll(new Task[]{ task1,task2});
                Console.WriteLine("主线程执行完毕!");
                Console.ReadKey();
            }
    复制代码

      执行结果如下:

      如果将栗子中的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执行完毕后就开始执行后续操作。看一个栗子:

    复制代码
            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) 来实现 ,修改上边栗子代码如下,执行结果不变。

    复制代码
           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 Thread取消任务执行

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

    复制代码
            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();
            }
    复制代码

    2 Task取消任务执行

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

    复制代码
            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会被执行。 看一个栗子:

    复制代码
            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,代码如下:

    复制代码
        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);
                    }
                });
            }
        }
    复制代码

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

    三、异步方法(async/await)

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

    复制代码
        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;
            }
        }
    复制代码

      test.txt内容是【hello world!】执行结果为:

      上边的栗子也写出了同步读取的方式,将main函数中的注释去掉即可同步读取文件内容。我们可以看到异步读取代码和同步读取代码基本一致。async/await让异步编码变得更简单,我们可以像写同步代码一样去写异步代码。注意一个小问题:异步方法中方法签名返回值为Task<TResult>,代码中的返回值为TResult。上边栗子中GetContentAsync的签名返回值为Task<string>,而代码中返回值为string。牢记这一细节对我们分析异步代码很有帮助。通过上边的栗子我们知道aysnc/await的用法十分简单,那么aynsc标记的异步方法是怎样执行的呢?什么时候创建的任务线程?再看一个简单的栗子:

    复制代码
        class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine(">>>>>>>>>>>>>>>>主线程启动");
                Task<string> task = GetStringAsync1();
                Console.WriteLine("<<<<<<<<<<<<<<<<主线程结束");
                Console.WriteLine($"GetStringAsync1执行结果:{task.Result}");
            }
    
            static async Task<string> GetStringAsync1()
            {
                Console.WriteLine(">>>>>>>>GetStringAsync1方法启动");
                string str = await GetStringAsync2();
                Console.WriteLine("<<<<<<<<GetStringAsync1方法结束");
                return str;
            }
            static async Task<string> GetStringAsync2()
            {
                Console.WriteLine(">>>>>>>>GetStringAsync2方法启动");
                string str = await GetStringFromTask();
                Console.WriteLine("<<<<<<<<GetStringAsync2方法结束");
                return str;
            }
    
            static Task<string> GetStringFromTask()
            {
                Console.WriteLine(">>>>GetStringFromTask方法启动");
                Task<string> task = new Task<string>(() =>
                 {
                     Console.WriteLine(">>任务线程启动");
                     Thread.Sleep(1000);
                     Console.WriteLine("<<任务线程结束");
                     return "hello world";
                 });
                task.Start();
                Console.WriteLine("<<<<GetStringFromTask方法结束");
                return task;
            }
        }
    复制代码

      使用async/await时,主线程和执行同步代码一样,是一路执行到底的,执行到await时也没有返回main函数,而是创建了一个空闲的任务后继续往下执行,当所有的调用的方法都执行完毕后再返回main函数。主线程执行到task.Start()才创建任务线程,真正开始执行异步任务。当异步方法async  Task<T>A()需要另一个异步方法async Task<T>B()的结果时,A()会等待B()完成异步任务再继续向下执行。程序执行结果如下:

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

  • 相关阅读:
    Java实现 蓝桥杯 算法训练 画图(暴力)
    Java实现 蓝桥杯 算法训练 画图(暴力)
    Java实现 蓝桥杯 算法训练 相邻数对(暴力)
    Java实现 蓝桥杯 算法训练 相邻数对(暴力)
    Java实现 蓝桥杯 算法训练 相邻数对(暴力)
    Java实现 蓝桥杯 算法训练 Cowboys
    Java实现 蓝桥杯 算法训练 Cowboys
    55. Jump Game
    54. Spiral Matrix
    50. Pow(x, n)
  • 原文地址:https://www.cnblogs.com/Alex80/p/10836023.html
Copyright © 2011-2022 走看看