欢迎来到学习摆脱又加深内卷篇
下面是学习异步编程的应用
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表达式完成,极其简洁,初学者可以多多尝试,不要避而远之。