C#进阶——从应用上理解异步编程的作用(async / await)

欢迎来到学习摆脱又加深内卷篇

下面是学习异步编程的应用

 

1.首先,我们建一个winfrom的项目,界面如下:

C#进阶——从应用上理解异步编程的作用(async / await)

 

 

 

2.然后先写一个耗时函数:

     /// <summary>
        /// 耗时工作
        /// </summary>
        /// <returns></returns>
        private string Work()
        {
            Thread.Sleep(1000); 
            Thread.Sleep(2000);
            //listBox1.Items.Add("耗时任务完成");
            return DateTime.Now.ToString("T") + "进入耗时函数里, 线程ID:" + Thread.CurrentThread.ManagedThreadId; //步骤7:子线程运行,不阻塞主线程
        }

这里用当前线程睡眠来模拟耗时工作

 

3.同步实现方式:

     
     private void button1_Click(object sender, EventArgs e)
        {
            listBox1.Items.Add(DateTime.Now.ToString("T") + "调用异步之前,线程ID:" + Thread.CurrentThread.ManagedThreadId); //步骤1:在主线程运行,阻塞主线程
            TaskSync(); 
            listBox1.Items.Add(DateTime.Now.ToString("T") + "调用异步之后,线程ID:" + Thread.CurrentThread.ManagedThreadId); //步骤2:在主线程运行,阻塞主线程
        }

        /// <summary>
        /// 同步任务
        /// </summary>
        private void TaskSync()
        {
            listBox1.Items.Add(DateTime.Now.ToString("T") + "同步任务开始,线程" + Thread.CurrentThread.ManagedThreadId);
            var resual = Work();
            listBox1.Items.Add(resual);
            listBox1.Items.Add(DateTime.Now.ToString("T") + "同步任务结束,线程" + Thread.CurrentThread.ManagedThreadId);
        }

 

运行结果:

C#进阶——从应用上理解异步编程的作用(async / await)

很明显以上就是同步实现方法,在运行以上代码时,会出现UI卡住了的现象,因为耗时工作在主线程里运行,所以UI一直刷新导致假死。

 

4.那么我们就会想到,可以开一个线程运行耗时函数,比如:

     private void button4_Click(object sender, EventArgs e)
        {
            listBox1.Items.Add(DateTime.Now.ToString("T") + "独立线程之前,线程" + Thread.CurrentThread.ManagedThreadId);
            ThreadTask();
            listBox1.Items.Add(DateTime.Now.ToString("T") + "独立线程之后,线程" + Thread.CurrentThread.ManagedThreadId);
        }

        /// <summary>
        /// 接收线程返回值
        /// </summary>
        class ThreadParm
        {
            /// <summary>
            /// 接收返回值
            /// </summary>
            public string resual = "耗时函数未执行完";

            /// <summary>
            /// 线程工作
            /// </summary>
            /// <returns></returns>
            public void WorkThread()
            {
                resual = Work();
            }

            /// <summary>
            /// 耗时工作
            /// </summary>
            /// <returns></returns>
            private string Work()
            {
                Thread.Sleep(1000);
                Thread.Sleep(2000);
                //listBox1.Items.Add("耗时任务完成");
                return DateTime.Now.ToString("T") + "进入耗时函数里, 线程ID:" + Thread.CurrentThread.ManagedThreadId; //步骤7:子线程运行,不阻塞主线程
            }
        }

        /// <summary>
        /// 独立线程任务
        /// </summary>
        private void ThreadTask()
        {
            listBox1.Items.Add(DateTime.Now.ToString("T") + "独立线程任务开始,线程" + Thread.CurrentThread.ManagedThreadId);
            ThreadParm arg = new ThreadParm();
            Thread th = new Thread(arg.WorkThread);
            th.Start();
            //th.Join();
            var resual = arg.resual;
            listBox1.Items.Add(resual);
            listBox1.Items.Add(DateTime.Now.ToString("T") + "独立线程任务结束,线程" + Thread.CurrentThread.ManagedThreadId);
        }

运行结果如下

C#进阶——从应用上理解异步编程的作用(async / await)

以上是开了一个线程运行耗时函数,用引用类型(类的实例)来接收线程返回值,主线程没有被阻塞,UI也没有假死,但结果不是我们想要的,

还没等耗时函数返回,就直接输出了结果,即我们没有拿到耗时函数的处理的结果,输出结果只是初始化的值

resual = "耗时函数未执行完";

为了得到其结果,可以用子线程阻塞主线程,等子线程运行完再继续,如下:

th.Join();
这样就能获得到耗时函数的结果,正确输出,但是在主线程挂起的时候,UI还是在假死,因此没有起到优化的作用。


5.可以把输出的结果在子线程(耗时函数)里输出,那样就主线程就不必输出等其结果了,既能输出正确的结果,又不会导致UI假死:
       /// <summary>
            /// 耗时工作
            /// </summary>
            /// <returns></returns>
            private void Work()
            {
                Thread.Sleep(1000);
                Thread.Sleep(2000);
                listBox1.Items.Add(("T") + "进入耗时函数里, 线程ID:" + Thread.CurrentThread.ManagedThreadId); //步骤7:子线程运行,不阻塞主线程
            }

如上修改耗时函数(其他地方修改我就省略了)再运行,会报如下错误:

C#进阶——从应用上理解异步编程的作用(async / await)

 

 

于是你会说,控件跨线程访问,这个我熟呀!不就用在初始化时添加下面这句代码吗:

Control.CheckForIllegalCrossThreadCalls = false;

又或者用委托来完成。

确实可以达到目的,但是这样不够优雅,而且有时候非要等子线程走完拿到返回结果再运行下一步,所以就有了异步等待

 

6.异步实现方式:

     /// <summary>
        /// 异步任务
        /// </summary>
        /// <returns></returns>
        private async Task TaskAsync()
        {
            listBox1.Items.Add(DateTime.Now.ToString("T") + "异步任务开始,线程ID:" + Thread.CurrentThread.ManagedThreadId); //步骤3:在主线程运行,阻塞主线程
            var resual = await WorkAsync();  //步骤4:在主线程运行,阻塞主线程

            //以下步骤都在等待WorkAsync函数返回才执行,但在等待的过程不占用主线程,所以等待的时候不会阻塞主线程
            string str = DateTime.Now.ToString("T") +   resual + "当前线程:" + Thread.CurrentThread.ManagedThreadId;
            listBox1.Items.Add(str);//步骤10:在主线程运行,阻塞主线程
            listBox1.Items.Add(DateTime.Now.ToString("T") + "异步任务结束,线程ID:" + Thread.CurrentThread.ManagedThreadId);//步骤11:在主线程运行,阻塞主线程
        }

        /// <summary>
        /// 异步工作函数
        /// </summary>
        /// <returns></returns>
        private async Task<string> WorkAsync()
        {
            listBox1.Items.Add(DateTime.Now.ToString("T") + "进入耗时函数前,线程" + Thread.CurrentThread.ManagedThreadId); //步骤5:在主线程运行,阻塞主线程

            //拉姆达表达式开异步线程
            //return await Task.Run(() =>
            //{
            //    Thread.Sleep(1000);
            //    //listBox1.Items.Add("计时开始:");
            //    Thread.Sleep(2000);
            //    //listBox1.Items.Add("计时结束");
            //    return "耗时:" + 30;
            //});

            //函数方式开异步现程
            string str = await Task.Run(Work); //步骤6:这里开线程处理耗时工作,不阻塞主线程,主线程回到步骤3

            //以下步骤都在等待Work函数返回才执行,但在等待的过程不占用主线程,所以等待的时候不会阻塞主线程
            listBox1.Items.Add(DateTime.Now.ToString("T") + "出去异步函数前,线程" + Thread.CurrentThread.ManagedThreadId); //步骤9:主线程运行,阻塞主线程
            return "运行时间" + str;
            //return await Task.Run(Work);
        }

        /// <summary>
        /// 耗时工作
        /// </summary>
        /// <returns></returns>
        private string Work()
        {
            Thread.Sleep(1000); 
            Thread.Sleep(2000);
            //listBox1.Items.Add("耗时任务完成");
            return DateTime.Now.ToString("T") + "进入耗时函数里, 线程ID:" + Thread.CurrentThread.ManagedThreadId; //步骤7:子线程运行,不阻塞主线程
        }

        private void button2_Click(object sender, EventArgs e)
        {
            listBox1.Items.Add(DateTime.Now.ToString("T") + "调用异步之前,线程" + Thread.CurrentThread.ManagedThreadId); //步骤1
            TaskAsync();//步骤2:调用异步函数,阻塞主线程
            listBox1.Items.Add(DateTime.Now.ToString("T") + "调用异步之后,线程" + Thread.CurrentThread.ManagedThreadId);
        }

运行结果如下:

C#进阶——从应用上理解异步编程的作用(async / await)

 

 

 

以上就能满足我们的需求,即不会卡UI,也能等待,且在等待结束后回到主线程运行。

其运行逻辑是:

C#进阶——从应用上理解异步编程的作用(async / await)

 

 

网上很多人说异步是开了线程来等待完成的, 从上图的时间轴来看,其并没有开启新的线程,都是同步往下执行。那为啥叫异步呢,因为执行到await时不发生阻塞,直接跳过等待去执行其他的,当await返回时,又接着执行await后面的代码,这一系列的运行都是在主调线程中完成,并没有开线程等待。所以如果耗时函数不开一个线程运行,一样会阻塞,没有完全利用异步的优势。

那么,await是在主线程等待,那其为什么没有阻塞主线程呢?我个人觉得其是利用委托的方式,后面再去揪原理吧!

 

其实异步编程很实用且优雅,特别结合lamda表达式完成,极其简洁,初学者可以多多尝试,不要避而远之。

 

原文作者:vv彭

原文连接:https://www.cnblogs.com/eve612/p/15778273.html

本文欢迎转载,转载标明出处!

 

上一篇:Python——协程操作


下一篇:Web自动化测试之playwright:概述