C#多线程和异步编程——Task和async/await详解

目录

一、什么是异步

二、Task介绍

1 Task创建和运行

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

三、异步方法(async/await)


一、什么是异步

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

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

二、Task介绍

  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的创建和执行方式有如下5种:

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();



            //4.  开启一个新线程,指定work方法
            Task.Factory.StartNew(Work);
            static void Work()
            {
                _running = true;

                while (_running)
                {
                    //SendOut();
                    //DealWith();
                    Thread.Sleep(50);
                }
            }

            //5. 这种方法也可以跨线程调用UI控件
            Action action = () =>
            {
               UiManager.SurfaceForm.SetSurplusPhotoNumber(count1.ToString());
            };
            Task.Factory.StartNew(action);



        }

 我们看到先打印"执行主线程",然后再打印各个任务,说明了Task不会阻塞UI主线程。上边的栗子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);//注意task.Result获取结果时会阻塞UI主线程
            Console.WriteLine(task2.Result);
            Console.WriteLine(task3.Result);
            Console.ReadKey();
        }

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

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

1 Thread阻塞线程的方法

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

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();
            //阻塞UI主线程
            th1.Join();
            th2.Join();
            Console.WriteLine("主线程执行完毕!");
            Console.ReadKey();
        }

2 Task的Wait/WaitAny/WaitAll方法

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

  task.Wait()  表示等待task执行完毕(会阻塞UI主线程),功能类似于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();
        }

三、异步方法(async/await,配对使用)

  C# 5 引入了一种简便方法,即异步编程。此方法利用了 .NET Framework 4.5 及更高版本、.NET Core 5.0 和 Windows 运行时中的异步支持.

测试环境:vs2019,.Net Framework4.6.1。

示例1:

        private void button1_Click(object sender, EventArgs e)
        {
            Console.WriteLine("主线程ManagedThreadId:" + Thread.CurrentThread.ManagedThreadId);
            AsyncTest();

        }
  
        static async void AsyncTest()
        {
            Console.WriteLine("*******Start************ManagedThreadId:" + Thread.CurrentThread.ManagedThreadId);
            Task<int> taskA = Print();
            Console.WriteLine("*********Middle**********ManagedThreadId:" + Thread.CurrentThread.ManagedThreadId);
            int a = await taskA;
            Console.WriteLine("return a=" + a);
            Console.WriteLine("*********End**********ManagedThreadId:" + Thread.CurrentThread.ManagedThreadId);
        }


        static Task<int> Print()
        {
            //Console.WriteLine("Print方法开始执行")
            return Task<int>.Run(() =>
            {
                Thread.Sleep(5000);
                return 98;
            });
        }

示例2

private async void button_Click(Object sender,EventArgs e)
{
    TextBox1.text=await testString();
}
private async Task<string> testString()
{
    
    return await Task<string>.Run)(()=>
    {
        Thread.Sleep(5000);
        return "test success";

    });
}

参考网址:

C#多线程和异步(二)——Task和async/await详解 - 捞月亮的猴子 - 博客园

C#中 Thread,Task,Async/Await,IAsyncResult 的那些事儿! - Mr靖 - 博客园

C# async与await的使用说明_codingriver的博客-CSDN博客

  • 3
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值