前言
    看ET框架时发现其广泛使用了ETVoid、ETTask的异步处理方式,同时也有段时间没复习await/async的有关内容了,所以也趁这个机会重新看一看。
| 12
 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
首先以一个代码例子来作为切入点
| 12
 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
其实这点在烟雨大佬的博客中已经写得比较详细了,我主要是在基础上加上一些理解。
备用链接
| 12
 
 | C# Task的GetAwaiter和ConfigureAwait:https://www.cnblogs.com/majiang/p/7908441.html
 
 | 
同样以一个代码例子作为切入点(A*寻路算法的测试部分截取):
| 12
 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