ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

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

2022-01-24 11:34:53  阅读:158  来源: 互联网

标签:进阶 Thread 耗时 C# Items await listBox1 Add 线程


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

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

 

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

 

 

 

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

 

复制代码

运行结果:

很明显以上就是同步实现方法,在运行以上代码时,会出现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);
        }
复制代码

运行结果如下

以上是开了一个线程运行耗时函数,用引用类型(类的实例)来接收线程返回值,主线程没有被阻塞,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:子线程运行,不阻塞主线程
            }
复制代码

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

 

 

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

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

运行结果如下:

 

 

 

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

其运行逻辑是:

 

 

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

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

 

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

 

本文来自博客园,作者:vv彭,转载请注明原文链接:https://www.cnblogs.com/eve612/p/15778273.html

努力逃出内卷!

努力卷!

 

转 https://www.cnblogs.com/eve612/p/15778273.html

标签:进阶,Thread,耗时,C#,Items,await,listBox1,Add,线程
来源: https://www.cnblogs.com/wl-blog/p/15838713.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有