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

  • 相关阅读:
    HDU 4069 Squiggly Sudoku
    SPOJ 1771 Yet Another NQueen Problem
    POJ 3469 Dual Core CPU
    CF 118E Bertown roads
    URAL 1664 Pipeline Transportation
    POJ 3076 Sudoku
    UVA 10330 Power Transmission
    HDU 1426 Sudoku Killer
    POJ 3074 Sudoku
    HDU 3315 My Brute
  • 原文地址:https://www.cnblogs.com/qtiger/p/13497807.html
Copyright © 2011-2022 走看看