为什么我们需要UniTask?谈论异步与Unity。

本文原本是在TDS写的一篇分享文章,现在我拿到我的博客上给大家分享一下。 我之前曾经提到过一个Unity插件叫做UniTask,是用来提供Unity异步操作用的。但是我想各位可能不太明确的一个问题是,为什么Unity需要使用到异步操作?或者还有人会问我,什么是异步?为了讲清楚这个东西,我将会从C#的异步和Unity多线程上的陷阱开始,为大家一步一步梳理UniTask应用场景。 多线程与异步 大家都知道线程这么一回事:程序运行的时候需要运行在至少一个线程上,才能保证程序中所定义的算法可以被执行。 而且稍微有经验的人也知道,操作多线程是一件比较麻烦的事情。一方面是线程安全问题——两个线程同时操作同一处内存时会引发不确定的操作结果——另一方面是如果一个线程需要依赖另外一个线程的结果,那我们还不停判断另外一个线程的状态来指导当前线程执行下一步算法的时机。 第一个问题相对而言会好解决一些:通过锁,保证多个线程操作同一块内存时,操作必定是原子的。但是第二个问题就会麻烦很多,其主要表现在以下方面: 状态机有可能会写错,增加debug成本。 会导致单个线程不断去判断另外一个线程的状态,使之空转浪费宝贵的CPU资源。 如果另外一个线程迟迟不能完成,这将会阻塞这个线程,有可能导致整个程序被卡死。 会让我们写的代码不够直观,增加编码成本(回调地狱)。 因此,为了解决上面的几个问题,我们提出了一种叫做异步的策略。 异步是什么 我觉得网上讲异步讲的都不够直接,我就直接的抛出我所认为异步的本质:异步是用来转让程序运行权的工具。我下面举几个具体的代码例子来示意: void Block() { ... // 做一件非常耗时间的事情 Console.WriteLine("Done"); } // 什么是async async Task BlockAsync() { await Task.Run(() => Block()); // 为什么这里有await return null; } void CallThem() { Block(); // 这个会导致执行这一个函数的线程被阻塞 var task = BlockAsync(); // 而这个却不会 Console.WriteLine("Called"); } 当我调用CallThem()这个函数时你会发现Block()会把这个程序卡死,导致你要等很久才能看到第一个“Done”被打印出来;但是你如果删掉CallThem()内的Block()调用,这个程序却在一瞬间就把“Called”给打印到控制台,不过要等很久控制台上面才会打印“Done”。这是为什么呢? Magic在于这条一行 await Task.Run(() => Block()); 在这行,await代表的是“我要转让运行权给调用我的函数”,也就是说将程序的运行权从函数BlockAsync转让回调用它的CallThem,从而让CallThem可以继续执行下去。不过你也需要注意一点,如果一个函数里面会出现await,那么其函数头必须使用async去标记,具体的缘由后面再说。 顺带一提,Task在这里指的是一个任务。需要注意的是只有可以返回Task、ValueTask或者TaskAwaiter的方法才可以使用await去转让运行权。同时,我们通过Task.Run()将我们的计算放置在了另外一个线程中,当被await的Task执行完成后,这个线程将会接着执行这个函数剩下的部分,直到它遇到了return或者await。 因此不难看出,通过await这一个魔法就能保证我们最大限度地不让程序被卡住,同时还能让我们尽可能榨干一个线程——干不完你我就先不鸟,你好了我再来鸟你。 我想要返回一个值 不过上面那句话还不太准确,因为我们还有可能需要这个函数所返回的值——说不准是我们求他干完而不是想不鸟就不鸟它——比如这样子: int Calc() { ... // 又要做一件非常耗时间的事情 return result; // result = 114514 } async Task<int> CalcAsync() { return Task....

2022/4/9 · Ca2didi