编程语言学习笔记C#|Unity C#(浅析同步异步与阻塞非阻塞与async关键字)

前言:作为一名新手,感觉同步异步与阻塞非阻塞这两对概念是在是长得太像了,网络上也是众说纷纭,接下来给出自己的理解吧,理解仅限于当前的知识范围,可能会有错误,还得继续改进。
话不多说,先贴几个比较高赞的帖子。
理解同步/异步和阻塞/非阻塞的区别_linhuaiyang的博客-CSDN博客_异步阻塞和同步阻塞的区别
怎样理解阻塞非阻塞与同步异步的区别? - 知乎
socket阻塞和非阻塞有哪些影响_mayue_csdn的博客-CSDN博客_非阻塞socket
完全理解同步/异步与阻塞/非阻塞 - 知乎
IO - 同步,异步,阻塞,非阻塞 (亡羊补牢篇)_historyasamirror的博客-CSDN博客_同步io和异步io的区别
同步/异步,阻塞/非阻塞概念深度解析_萧萧九宸的博客-CSDN博客_同步和异步阻塞和非阻塞
微软官方举的异步的例子(举得复杂了,容易让人找不到本质):C# 中的异步编程 | Microsoft Docs
跟其他例子:【转载】C#之异步 - 走看看
关于C#异步编程你应该了解的几点建议
最后。总结上述材料,我感觉同步异步就是更强调消息的通知,而阻塞非阻塞更强调任务的执行,一般来说非阻塞就是异步的,但异步不一定非阻塞,而阻塞和同步则是同义词。当然,消息的通知和任务的执行取决于我们是怎么定义的。讲的有点复杂,还是举例子理解理解。
注意:以下程序是在unity中运行的,会用到unity的生命周期。并且会用到C#的async关键字,需要读者补一补。
一、既是同步又是阻塞的例子

using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using System.Threading; public class ProgressBar : MonoBehaviour { void Start() { MyMain(); Debug.Log("Start OK,Continue Next Work"); }void MyMain()//假如MyMain是个任务 { Debug.Log("begin"); //任务调用开启 var ww1 = WaitWait1(); //开启任务1 var ww0 = WaitWait0(); //开启任务0 Debug.Log("end"); //任务调用结束 Debug.Log(ww0); //通知wwo当前值 Debug.Log(ww1); //通知ww1当前值 Debug.Log("Function End"); //函数终止任务 } int WaitWait0() { Debug.Log("will wait 1s 0 0"); Task.Delay(1000).Wait(); //任务0需要耗时1s Debug.Log("wait Success 0 0"); return 2; } int WaitWait1() { Debug.Log("will wait 3s 1 1"); Task.Delay(3000).Wait(); //任务1需要耗时3s Debug.Log("wait Success 1 1"); return 3; }}

毫无疑问,上面的代码是一句一句执行的,并且能很明显感受到启动游戏的时候,卡了那么一会。这说明了Start函数在等执行MyMain都执行完才结束(耗时4s),才进行Update开启游戏运行。而MyMain得等WaitWait1和WaitWait0这两任务按顺序执行完再按顺序挨个继续通知ww0和ww1的当前值。这不就是又阻塞又同步吗。
编程语言学习笔记C#|Unity C#(浅析同步异步与阻塞非阻塞与async关键字)
文章图片

看截图要注意看时间点:这几个数据是要敏感的,21-18=3秒,22-21=1秒。整个过程耗时4秒
二、异步但阻塞的例子? 因为我也不太清楚是不是阻塞还是非阻塞的,所以加了个?,个人暂且认为他是阻塞的。
using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using System.Threading; public class ProgressBar : MonoBehaviour { void Start() { MyMain(); Debug.Log("Start OK,Continue Next Work"); }async Task MyMain() { Debug.Log("begin"); //任务调用开启 var ww0 = WaitWait2(); //异步开启任务2 var ww1 = WaitWait3(); //异步开启任务3 Debug.Log("end"); //任务调用结束 Debug.Log(await ww0); //异步通知wwo当前值 Debug.Log(await ww1); //异步通知ww1当前值 Debug.Log("Function End"); //函数终止任务 } async Task WaitWait2() { Debug.Log("will wait 3s 2"); await Task.Delay(3000); //任务2需要耗时3s Debug.Log("wait Success 2"); return 2; } async Task WaitWait3() { Debug.Log("will wait 1s 3"); await Task.Delay(1000); //任务3需要耗时1s Debug.Log("wait Success 3"); return 3; }}

还是看执行结果来分析:在MyMain没执行完就开始继续Start后的代码了,这说明相对Start函数,MyMain是异步非阻塞的。
接下来分析MyMain里面的。在MyMain里面,WaitWait2和WaitWait3显然是异步开启的,尽管是WaitWait2先开启,但是并不阻塞WaitWait3的执行,并且WaitWait3先提前结束任务,用时1s,WaitWait3用时3s后结束任务。在整个流程中,WaitWait3与WaitWait2是并行异步的,整个MyMain任务用时3s。
但为什么说这个是阻塞的呢?因为相对Debug.Log("Function End")这个函数终止的任务来说,他是需要等到两个异步的通知都收到了才进行的,因此我认为他在这是阻塞的。(并且注意通知ww0和ww1的当前值也是按顺序同步阻塞的)
编程语言学习笔记C#|Unity C#(浅析同步异步与阻塞非阻塞与async关键字)
文章图片

然后,为了对这个await理解更深刻点,我们把上面的await的位置改一改。
using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using System.Threading; public class ProgressBar : MonoBehaviour { void Start() { MyMain(); Debug.Log("Start OK,Continue Next Work"); }async Task MyMain() { Debug.Log("begin"); //任务调用开启 var ww0 =await WaitWait2(); //开启任务2 var ww1 =awaitWaitWait3(); //开启任务3 Debug.Log("end"); //任务调用结束 Debug.Log(ww0); //顺序通知wwo当前值 Debug.Log(ww1); //顺序通知ww1当前值 Debug.Log("Function End"); //函数终止任务 } async Task WaitWait2() { Debug.Log("will wait 3s 2"); await Task.Delay(3000); //任务2需要耗时3s Debug.Log("wait Success 2"); return 2; } async Task WaitWait3() { Debug.Log("will wait 1s 3"); await Task.Delay(1000); //任务3需要耗时1s Debug.Log("wait Success 3"); return 3; }}

看下面结果,这个时候相对Start还是异步非阻塞的,但是对WaitWait2和WaitWait3来说却是同步阻塞的,WaitWait3需要等WaitWait2都运行完才会开启,因此整个MyMain任务用时4秒。
从这也可以看出await具有局部阻塞的作用,在程序的某个位置加await关键字,则会阻塞,需要等待任务完成才进行下一步。但是使用await是不会使MyMain在Start中阻塞,仅是局部阻塞。
编程语言学习笔记C#|Unity C#(浅析同步异步与阻塞非阻塞与async关键字)
文章图片


三、异步非阻塞 我想了想,要想做到异步非阻塞,只有用异步回调或者多线程了。并且C#中实现异步的task是由线程池实现的,不过C#帮我们封装好了,能让我们很好地掌控他,具体可看微软官方文档:Task表示一个可以返回值的异步操作。 编程语言学习笔记C#|Unity C#(浅析同步异步与阻塞非阻塞与async关键字)
文章图片
https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task-1?view=net-6.0
关于task还可以参考:c#异步编程-Task(一) - 知乎
【编程语言学习笔记C#|Unity C#(浅析同步异步与阻塞非阻塞与async关键字)】接下来用多线程来举例异步非阻塞吧,这样可能理解起来会更舒服:
using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using System.Threading; public class ProgressBar : MonoBehaviour { void Start() { MyMain(); Debug.Log("Start OK,Continue Next Work"); } int ww1Thread = 0; int ww0Thread = 0; void MyMain() { Debug.Log("begin"); Thread thread1 = new Thread(WaitWait4); Thread thread2 = new Thread(WaitWait5); thread1.Start(); //在线程1开启任务1 thread2.Start(); //在线程2开启任务2 Debug.Log("end"); Debug.Log(ww1Thread); //通知ww1Thread当前值 Debug.Log(ww0Thread); //通知ww0Thread当前值 Debug.Log("Function End"); //函数终止任务 } void WaitWait4() { Debug.Log("will wait 1s 4"); Task.Delay(1000).Wait(); Debug.Log("wait Success 4"); ww0Thread = 2; Debug.Log(ww0Thread); //回调通知ww0Thread当前值 } void WaitWait5() { Debug.Log("will wait 3s 5"); Task.Delay(3000).Wait(); Debug.Log("wait Success 5"); ww1Thread = 3; Debug.Log(ww1Thread); //回调通知ww1Thread当前值 }}

这个程序就很顺畅了,所有的全都是并行的,Debug.Log("Function End")这个任务也不用等WaitWait4和WaitWait5任务执行完就可以执行了。但是缺点就是为了解决阻塞Debug.Log("Function End"),需要将通知放到线程中进行回调。
最终整个任务的运行时间也是3秒,这样想想确实不如直接用task方便。
编程语言学习笔记C#|Unity C#(浅析同步异步与阻塞非阻塞与async关键字)
文章图片

四、总结 说了那么多,感觉我自己对同步异步跟阻塞非阻塞还是没太弄明白,不过代码总是对的,我们也不必拘泥于某个概念钻牛角尖,还是得以实际需求为主。

    推荐阅读