zoukankan      html  css  js  c++  java
  • C#进阶——从应用上理解异步编程的作用(async / await)

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

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

    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

    努力逃出内卷!

    努力卷!

  • 相关阅读:
    Linux性能监测:磁盘IO篇
    Linux性能监测:网络篇
    Linux性能监测:内存篇
    Linux系统和性能监控之CPU篇
    linux 进程和线程讲解
    web-单机时代
    自动化-ELK日志管理
    自动化--zabbix-proxy
    自动化-zabbix(2)
    IOS中的用户安全
  • 原文地址:https://www.cnblogs.com/eve612/p/15778273.html
Copyright © 2011-2022 走看看