前言
看ET框架时发现其广泛使用了ETVoid、ETTask的异步处理方式,同时也有段时间没复习await/async的有关内容了,所以也趁这个机会重新看一看。
1 2 3 4 5 6 7 8
| 4.1ETTask: https://www.yuque.com/et-xd/docs/wyr682 ET篇:ETVoid和void,ETTask和Task的区别与使用时机: https://www.lfzxb.top/et-ettask-etvoid/ C# async 和 await 理解: https://blog.csdn.net/a462533587/article/details/82261468 字母哥:【ET框架课程】08-异步编程与ETTask的使用 https://www.bilibili.com/video/BV1sV411J7wJ?spm_id_from=333.999.0.0
|
总结
- ET方面:
- ET框架是单线程逻辑,ETTask是一个轻量级单线程的Task,相比Task性能更强,本质上可以说是协程
- ETTask就是把回调改成同步的写法,具体是单线程回调还是多线程回调都与ETTask无关
- ETVoid是代替async void ,意思是新开一个协程
- ETTask的Coroutine方法是为了无GC,ETTask必须await或者调用coroutine才能回收重用ETTaskCompletionSource
- await/task方面:
- Task本身与多线程无关,而Task.Run()等创建函数中则会将线程池中的线程分配给创建出来的Task
- await本身与多线程无关,只是会在async函数中根据await切分为几段,做成一个状态机,将其中的每一段都用一个task来分割,在这个task.Complete被执行的时候将状态机.next()方法压入到同步上下文中,最后调用状态机.Next()执行await之后的流程。
await和async
首先以一个代码例子来作为切入点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;
namespace asyncTest { class Program { static void Main(string[] args) { Test(); Console.ReadKey(); }
public static async void Test() { //MARKER:这里创建了一个新的状态机来划分await Console.WriteLine("main:"+AppDomain.GetCurrentThreadId().ToString()); await Method4(); //这里做了第一次分割,下一行会用新的task执行 Method1(); //开启一个新的Task Task.Run(() => { Console.WriteLine("new task:"+AppDomain.GetCurrentThreadId().ToString()); }); Method2(); await Method1(); //同理 Method3(); }
public static async Task Method1() { Console.WriteLine("method 1:"+AppDomain.GetCurrentThreadId().ToString()); await Task.Run(() => { for (int i = 0; i < 10; i++) { Console.WriteLine(" Method 1"); } });
for (int i = 0; i < 10; i++) { Console.WriteLine("After-------"); } }
public static async Task Method4() { Console.WriteLine("method 4:"+AppDomain.GetCurrentThreadId().ToString()); await Task.Run(() => { for (int i = 0; i < 10; i++) { Console.WriteLine(" Method 4"); } }); }
public static void Method2() { Console.WriteLine("Method2:"+AppDomain.GetCurrentThreadId().ToString()); for (int i = 0; i < 30; i++) { Console.WriteLine(" Method 2"); } }
public static void Method3() { Console.WriteLine("Method3:" + AppDomain.GetCurrentThreadId().ToString()); for (int i = 0; i < 30; i++) { Console.WriteLine(" Method 3"); } } } }
|
得到的结果为:
我们可以来捋一捋发生的情况:
- 主线程的线程id为14452,而在等待第一次await结束前的Method4()函数也同时在主线程中执行
- 之后的Method1()、Method2()、await Method1() 则被划分到了新的Task内执行,因此三者的线程ID都为新的随机分配的ID,而Task.Run()作为Task创建函数则在创建时根据线程池给task分配了线程ID
- 最后的Method3()原因与第二点相同,其被划分到了新的Task内执行,因此其线程ID为新的随机分配的ID
ETVoid与ETTask
其实这点在烟雨大佬的博客中已经写得比较详细了,我主要是在基础上加上一些理解。
备用链接
1 2
| C# Task的GetAwaiter和ConfigureAwait: https://www.cnblogs.com/majiang/p/7908441.html
|
同样以一个代码例子作为切入点(A*寻路算法的测试部分截取):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| //测试函数 public static async ETTask NodeGOHandle(this GridComponent self, NodeItem node) { //mapHolder中找到对应GO GameObject go = self.GetNodeGO(node);
go.transform.Translate(go.transform.InverseTransformDirection(0,0,2));//稍微前移 Debug.Log($"wait for x:{node.x},y:{node.y}"); //await TimerComponent.Instance.WaitAsync(2000); await self.test(); Debug.Log($"finish for x:{node.x},y:{node.y}"); go.transform.Translate(go.transform.InverseTransformDirection(0,0,-2));//稍微前移 }
public static async ETTask test(this GridComponent self) { for (int i = 0; i < 100; i++) { Debug.Log("Test"+i); await TimerComponent.Instance.WaitAsync(100); } await ETTask.CompletedTask; }
//根据生成找到的路径生成具体的路径展示 public static void GeneratePath(this GridComponent self,NodeItem startNode,NodeItem endNode) { NodeItem curNode = endNode; List<NodeItem> nodes = new List<NodeItem>(); int count = 0; while (curNode != startNode) { Debug.Log($"count:{++count}"); if(count==1) self.NodeGOHandle(curNode).Coroutine(); // nodes.Add(curNode); curNode = curNode.parent; }
nodes.Reverse(); self.SetPath(nodes); }
|
放上输出的结果:
现在我们来逐步分析:
- 调用GeneratePath时,首先输出count:1,然后进入到self.NodeGoHandle(curNode)的异步函数中
- 顺序执行NodeGOHandle中await前的部分函数,输出wait for……,进入到self.test()函数
- 顺序执行self.test()函数,输出Test0,此时碰到waitAsync(100),因为waitAsync为异步函数且具有阻塞情况,此时会执行外面将要执行的函数
△为什么前面的NodeGoHandle和test都进行了顺序执行,而最后test则返回到原调用函数的后续部分呢?
- 如果一个异步函数内部仅有await ETTask.CompletedTask或者其嵌套await的函数里面仅有await ETTask.CompletedTask,可以单纯看做一个同步函数。在这种情况下无论是.Coroutine()还是await都与同步函数调用没有区别
- 而在例子里,async异步函数test()中调用了waitAsync(),其是阻塞的(对于test()来说),因此会返回到原调用函数的后续部分继续执行 且 等待其任务执行完毕后压入test()函数中的后续部分
异同点:
共同点
不同点
- ETTask可以有返回值(<T>作为泛型即可);可以等待返回结果(具有getAwaiter以及实现awaiter对应接口);可以通过.Coroutine()执行无需等待返回的异步,也可以通过await执行需要等待异步返回才能继续的情况
- ETVoid不能有返回值,不可以等待返回结果(即不能用await),也无法等待自身内部任务完成后再执行下面的语句
使用情况:
- ETTask
- 通常用于ET的EventSystem中的事件函数,同时也用于UI创建的异步函数(ETTask<UI>),其本身可以通过返回值返回异步创建后的ui
- ETVoid
- 通常用于ET中的网络交互的框架,因为基本不可能存在await服务器消息的情况(如果服务器崩了那客户端也会直接卡死),具体可以看C/S中的一些处理网络信息的handler