Thread,ThreadPool,Task (异步调用 async await )

程序中经常用到的同步和异步方法

同步:当一个方法被调用以后,调用者需等待该方法执行完毕以后方能再次调用

异步:当调用者调用一个方法时,会分配一个线程去处理方法内部业务逻辑,当在有其他调用者调用时,调用者不必等该方法执行是否完毕,就可调用

同步异步调用的应用场景

同步: 在需要执行方法需要立即出结果的,或者被调用者调用等结果返回值才能进行下一步操作的,都需要同步执行

异步:在不需要立即响应结果的,被执行的程序中基本没有关联的场景下。好处是非阻塞,因此可以把一些不需要立即使用结果,较耗时的任务设计为异步执行

可以提升程序运行效率。net4.0在ThreadPool的基础上推出了Task类,微软也极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;

net5.0推出了async/await,让异步编程更为方便。

本文主要介绍Task、async\await相关内容

Task简介

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

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



调用以后执行结果

 

 

 从结果可以看出ThreadPool相对于Thread来说可以减少线程的创建,有效减少系统开销;但是ThreadPool不能控制线程的执行顺序,我们也不能获取线程池

内线程取消/异常/完成的通知,即我们不能有效监控和控制线程池中的线程,基于此我们来研究一下Task

Task创建和运行

net4.0在ThreadPool的基础上推出了Task,Task拥有线程池的优点,同时也解决了使用线程池不易控制的弊端

创建并运行Task,有三种方式方法:

        static void Main(string[] args)
        {
            CreatTaskMethod();
            Console.ReadKey();
        }public static void CreatTaskMethod()
        {
            //方式一:
            Task task = new Task(()=> {
                Thread.Sleep(100);
                Console.WriteLine($"Task第一种创建方式:通过实例化一个Task,然后通过Start方法启动,当前线程Id为{Thread.CurrentThread.ManagedThreadId}");
            });
            task.Start();
            //方式二:
            Task task2 = Task.Factory.StartNew(() => {
                Thread.Sleep(100);
                Console.WriteLine($"Task第二种创建方式:直接创建并启动Task,当前线程Id为{Thread.CurrentThread.ManagedThreadId}");
            });
            //方式三:
            Task task3 = Task.Run(()=> {
                Thread.Sleep(100);
                Console.WriteLine($"Task第三种创建方式:将任务放在线程池队列,返回并启动一个Task,当前线程Id为{Thread.CurrentThread.ManagedThreadId}");
            });
            Console.WriteLine("主线程创建");

        }

执行结果如下:

 可以看到线程的创建 并不影响主线程的创建,说明Task任务不会阻塞主线程,我们也可以创建带有返回值的Task<TResult>,创建方法和没有返回值的基本一样

代码如下:

        static void Main(string[] args)
        {
            CreatTaskMethod();
            Console.ReadKey();
        }

        /// <summary>
        /// 创建带有返回值的Task任务
        /// </summary>
        public static void CreatReturnTask()
        {
            //方式一:
            Task<string> task = new Task<string>(() => {
               return ($"Task第一种创建方式:通过实例化一个Task,然后通过Start方法启动,当前线程Id为{Thread.CurrentThread.ManagedThreadId}");
            });
            task.Start();
            //方式二:
            Task<string> task2 = Task<string>.Factory.StartNew(() => {
                return ($"Task第二种创建方式:直接创建并启动Task,当前线程Id为{Thread.CurrentThread.ManagedThreadId}");
            });
            //方式三:
            Task<string> task3 = Task<string>.Run(() => {
              return ($"Task第三种创建方式:将任务放在线程池队列,返回并启动一个Task,当前线程Id为{Thread.CurrentThread.ManagedThreadId}");
            });
            Console.WriteLine("主线程创建");
            Console.WriteLine(task.Result);
            Console.WriteLine(task2.Result);
            Console.WriteLine(task3.Result);
        }

执行结果:

 

 

注意:  task.Result 获取结果时会阻塞线程,即如果task没有执行完成,会等待task执行完成以后获取结果Result,然后在执行后面的代码

可以把 Console.WriteLine("主线程创建"); 放到现在获取Result结果后面会的到

 

 

这里会发现主线程被阻塞了。

 

Task也可以同步执行,不会阻塞主线程。Task提供了 RunSynchronously()用于同步执行Task任务,代码如下:

public static void SyncTask()
        {
            for (int i = 0; i < 10; i++)
            {
                Task task = new Task(() => {
                    Thread.Sleep(100);
                    Console.WriteLine($"执行结果Task结束,当前执行顺序为{i}");
                });
                task.RunSynchronously();

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

 

执行结果如下:

 

 

 

 

 这里可以看到 Task执行线程是有顺序的,在使用RunSynchronously以后,会阻塞主线程,达到同步执行。除了使用RunSynchronously方法阻塞线程外,我们还可以使用(wait/waitAll/waitAny)阻塞Task

这里先说明一下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();
        }

Task的Wait/WaitAny/WaitAll方法

Thread的join 方法可以阻塞调用线程,但弊端是不利于调用,如果需要阻塞的线程比较多,需要每个线程都要调用一次 Thread.join() 方法;

另外如果我们想要某一线程或者全部线程执行完毕以后,立即解除线程阻塞,使用 join不容易实现,而Task提供了这种机制,下面我们逐一解释一下

Wait()  表示等待Task执行完毕,功能类似于thread,join();  

WaitAll(Task[] tasks) 表示只有所有的task都执行完毕以后在接触阻塞;

WaitAny(Task[] tasks) 表示只要有一个task执行完毕就解除阻塞。列:

        public static void TaskWaitAll()
        {
            Task task = Task.Factory.StartNew(() => {
                Console.WriteLine("线程1执行完毕");
            });
            Task task1 = Task.Run(() => {
                Console.WriteLine("线程2执行完毕");
            });
            Task.WaitAll(new Task[] { task,task1 });
            Console.WriteLine("主线程执行完毕!");
        }

执行结果如下:

 

 

 可以看到线程是异步执行在我们指定了WaitAll线程之后 主线程是出于阻塞状态的,当WaitAll下所有线程执行完毕以后,主线程才解除阻塞,执行,如果使用了

WaitAny 执行得到的结果就是 当线程一或者线程二任意一个执行完毕的时候 主线程就会解除阻塞,继续进行

注意:Wait 、WaitAll、WaitAny方法返回值都是void 这些方法单纯的实现阻塞线程。

如果想要让所有Task执行完毕,或则让任意Task执行完毕后,开始解除阻塞执行后续操作,该如何实现呢?这时候需要用到WhenAny、WhenAll方法了这些方法执行完毕以后会返回一个task实力。

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

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

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

 

        static void Main(string[] args)
        {
            TaskWhenAll();
            Console.ReadKey();
        }
        /// <summary>
        /// WhenAll  WhenAny
        /// </summary>
        public static void TaskWhenAll()
        {
            Task task1 = Task.Run(() => {
                Console.WriteLine("线程1执行完毕");
            });
            Task task2 = Task.Factory.StartNew(() => {
                Console.WriteLine("线程2执行完毕");
            });
            //Task.WhenAll(task1, task2).ContinueWith((t) => {
            //    Console.WriteLine("执行后续操作");
            //});
            Task.WhenAny(task1, task2).ContinueWith((t) => {
                Console.WriteLine("任意线程执行完毕后执行");
            });
            Console.WriteLine("主线程执行完毕");
        }

 

 

执行结果如下:

 

可以看到使用WhenAll、WhenAny时方法不会阻塞线程

上边的例子也可以通过Task.Factory.ContinueWhenAll(Task[] tasks,Action continuationAction) 和 Task.Factory.ContinueWhenAny(Task[] task, Action continuationAction)实现 例:

        public static void TaskWhenAll()
        {
            Task task1 = Task.Run(() => {
                Console.WriteLine("线程1执行完毕");
            });
            Task task2 = Task.Factory.StartNew(() => {
                Console.WriteLine("线程2执行完毕");
            });
            //Task.WhenAll(task1, task2).ContinueWith((t) => {
            //    Console.WriteLine("执行后续操作");
            //});
            //Task.WhenAny(task1, task2).ContinueWith((t) => {
            //    Console.WriteLine("任意线程执行完毕后执行");
            //});
            Task task3 = Task.Factory.ContinueWhenAll(new Task[] { task1,task2}, (t) => {
                Console.WriteLine("线程全部执行完毕以后,执行");
            });
            Task task4 = Task.Factory.ContinueWhenAny(new Task[] { task1,task2 },(t)=> {
                Console.WriteLine("任意线程执行完毕以后,执行");
            });
            Console.WriteLine("主线程执行完毕");
        }

这两种写法不同 但是功效是相同的

 Task的任务取消

先看一下 Thread是如何取消任务执行的

1 设置变量来控制任务是否停止,如var isStop=false; 如果值为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();
        }

Task是如何取消任务执行的呢?Task中有一个专门的类CancellationTokenSource 来取消任务执行,还是上面的例子,修改后如下:

        /// <summary>
        /// 取消任务
        /// </summary>
        public static void CancelTask()
        {
            CancellationTokenSource source = new CancellationTokenSource();
            int index = 0;
            Task task = Task.Factory.StartNew(() => {
                while (!source.IsCancellationRequested)
                {
                    Console.WriteLine($"第{++index}次执行,线程运行中......");
                }
            });
            //0.1秒后取消任务执行
            //取消方法1:
            Thread.Sleep(100);
            source.Cancel();//这个时候IsChancellationRequested会变成true 还可以用 
            //取消方法2:
            //source.CancelAfter(100); //这个是0.1秒后自动取消的,和上面方法是等价的

        }

了解了线程创建,执行,销毁,那么跨线程操作也是很有必要了解一下的

跨线程执行

 public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Task.Run(() => {
                Action<int> setValue = (p) => { textBox1.Text = p.ToString(); };
                for (int i = 0; i < 1000000; i++)
                {
                    textBox1.Invoke(setValue,i);
                }
            });
        }
    }

异步方法(async/await)

在C#5.0中出现的,让异步编程变得更简单。我们看一个例子:

        static void Main(string[] args)
        {
            MethodOne();
            MethodTwo();
            Console.ReadKey();
        }

        public static async Task MethodOne()
        {
            await Task.Run(() => {
                for (int i = 0; i < 50; i++)
                {
Thread.Sleep(100); Console.WriteLine(
"111"); } }); } public static void MethodTwo() { for (int i = 0; i <30; i++) {
Thread.Sleep(100); Console.WriteLine(
"222"); } }

执行结果

 

 可以看到虽然先调用了方法一 但是方法二 由于是异步 也没影响方法1的执行

 

 

 

 

 

posted @ 2020-08-21 16:28  秋风野马  阅读(405)  评论(0编辑  收藏  举报