前言

​ 看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");
}
}
}
}

得到的结果为:

image-20211023143024534

image-20211023143130372

我们可以来捋一捋发生的情况:

  • 主线程的线程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);
}

放上输出的结果:

image-20211027132856582

现在我们来逐步分析:

  • 调用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()函数中的后续部分

异同点:

  • 共同点

    • 都具有async修饰符,即都代表了异步函数
  • 不同点

    • ETTask可以有返回值(<T>作为泛型即可);可以等待返回结果(具有getAwaiter以及实现awaiter对应接口);可以通过.Coroutine()执行无需等待返回的异步,也可以通过await执行需要等待异步返回才能继续的情况
    • ETVoid不能有返回值,不可以等待返回结果(即不能用await),也无法等待自身内部任务完成后再执行下面的语句

使用情况:

  • ETTask
    • 通常用于ET的EventSystem中的事件函数,同时也用于UI创建的异步函数(ETTask<UI>),其本身可以通过返回值返回异步创建后的ui
  • ETVoid
    • 通常用于ET中的网络交互的框架,因为基本不可能存在await服务器消息的情况(如果服务器崩了那客户端也会直接卡死),具体可以看C/S中的一些处理网络信息的handler