这篇文章主要解释ET服务器框架中的ETTak相关,异步相关,async与await相关
首先要说的是,async与await是C#编译器给的一种语法糖。使得大家可以以一种同步写代码的方式,实现异步处理。简单的来说,就是我现在有两个任务,1.开洗衣机洗衣服,2.我要吃饭。所以正常方式是, 我开洗衣机,等它洗完衣服,就把衣服拿出来晒,晒完了,去吃饭,这就是同步。而聪明的人肯定是,我开洗衣机,然后去吃饭,等衣服洗完了,洗衣机会发出完成的声音,告诉我该停下吃饭,继续把衣服拿出来晒了,晒完了可以继续吃饭。这样大大节约了时间。
回调是一种将方法当作参数传给其他方法函数使用,并在合适时机进行调用的机制。这种回调就相当于上面洗衣机洗完了,告诉我。
而平时可能回调一两次,我们觉得很简单,但是如果多了,那就是回调地狱了。曾经维护过一个界面关闭的各种回调,各种恶心。现在C#提供了async与await的语法糖,帮助开发者大大简化了开发异步的写法。类似下面这种伪代码:
定义方法:async 洗衣服() //这里可能需要半个小时
定义方法:晒衣服()
定义方法:吃饭()
定义方法:搞家务(){await 洗衣服;晒衣服}
主方法:干活{搞家务(),吃饭()}
上面干活的时候,开了洗衣机就马上可以吃饭了,洗衣服可能需要半个小时,半个小时后就直接接着搞家务洗衣服了。不需要把吃饭的回调放到搞家务里面,然后洗衣机开了,回调吃饭,然后洗完衣服回调晒衣服了。
不知道上面的例子讲清楚没有。。。姑且就这么看着吧,下面用实际代码来说下ETTask的相关内容。
async,await,编译器会帮助我们内部生成一个状态机,用于管理各个回调与处理。直接拿ET中比较常见的登录C2R_LoginHandler中的Run方法来说。
[MessageHandler(AppType.Realm)]
public class C2R_LoginHandler : AMRpcHandler<C2R_Login, R2C_Login>
{
protected override async ETTask Run(Session session, C2R_Login request, R2C_Login response, Action reply)
{
// 随机分配一个Gate
StartConfig config = Game.Scene.GetComponent<RealmGateAddressComponent>().GetAddress();
//Log.Debug($"gate address: {MongoHelper.ToJson(config)}");
IPEndPoint innerAddress = config.GetComponent<InnerConfig>().IPEndPoint;
Session gateSession = Game.Scene.GetComponent<NetInnerComponent>().Get(innerAddress);
// 向gate请求一个key,客户端可以拿着这个key连接gate
G2R_GetLoginKey g2RGetLoginKey = (G2R_GetLoginKey)await gateSession.Call(new R2G_GetLoginKey() {Account = request.Account});
string outerAddress = config.GetComponent<OuterConfig>().Address2;
response.Address = outerAddress;
response.Key = g2RGetLoginKey.Key;
reply();
}
}
上面有一个await gateSession.Call,咱们用反编译工具来看下编译器给我们生成的代码如下:
可以看到里面帮我们生成了一个状态机d__0类,继承了IAsyncStateMachine,里面把之前的原始定义的字段与属性,都取了另外的名字,核心代码:MoveNext(),打开看到如下:
这段代码确实看起来很可怕,各种乱七八糟的变量名,过程也很长,但是不要怕,咱们只看关键流程就好。
public ETTask<IResponse> Call(IRequest request)
{
int rpcId = ++RpcId;
var tcs = new ETTaskCompletionSource<IResponse>();
this.requestCallback[rpcId] = (response) =>
{
if (ErrorCode.IsRpcNeedThrowException(response.Error))
{
tcs.SetException(new Exception($"Rpc Error: {request.GetType().FullName} {response.Error}"));
return;
}
tcs.SetResult(response);
};
request.RpcId = rpcId;
this.Send(request);
return tcs.Task;
}
可以看到返回的是一个ETTaskCompletionSource的Task,相关代码:public ETTask
public ETTask(IAwaiter<T> awaiter)
{
this.result = default;
this.awaiter = awaiter;
}
这里就把上面的ETTaskCompletionSource传给ETTask的awaiter了,(要记住这一点,因为好几个awaiter在里面。)
这个ETTask类定义的时候加了
这里就表明了,这个ETTask生成状态机时,里面的builder用的AsyncETTaskMethodBuilder这个类。
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
stateMachine.MoveNext();
}
很简单就是调用传入的状态机的moveNext方法。
awaiter = gateSession53.Call((IRequest) r2GGetLoginKey).GetAwaiter();
这句;可以看到Call返回的是ETTaskCompletionSource类里面的ETTask实例。然后ETTask实例的GetAwaiter()代码如下:public Awaiter GetAwaiter()
{
return new Awaiter(this);
}
就是直接new了一个Awaiter对象,注意区别ETTask里面的private readonly IAwaiter awaiter;
这个awaiter是上面传入的ETTaskCompletionSource对象实例:public ETTask
public Awaiter(ETTask task)
{
this.task = task;
}
[DebuggerHidden]
public bool IsCompleted => task.IsCompleted;
ETTask中的:
public bool IsCompleted => awaiter?.IsCompleted ?? true;
ETTaskCompletionSource中的
bool IAwaiter.IsCompleted => state != Pending;
最终就是看ETTaskCompletionSource对象实例的state状态是否完成。
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine
{
if (moveNext == null)
{
if (this.tcs == null)
{
this.tcs = new ETTaskCompletionSource<T>(); // built future.
}
var runner = new MoveNextRunner<TStateMachine>();
moveNext = runner.Run;
runner.StateMachine = stateMachine; // set after create delegate.
}
awaiter.UnsafeOnCompleted(moveNext);
}
其中内部封装了一个MoveNextRunner,runner.Run其实就是状态机的moveNext,调用awaiter的UnsafeOnCompleted,并将moveNext传进去,在ET内就是将状态机的moveNext传给了ETTaskCompletionSource了continuation(这个回调很重要):
MoveNextRunner:
internal class MoveNextRunner<TStateMachine> where TStateMachine : IAsyncStateMachine
{
public TStateMachine StateMachine;
//[DebuggerHidden]
public void Run()
{
StateMachine.MoveNext();
}
}
Awaiter内:
public void UnsafeOnCompleted(Action continuation)
{
if (task.awaiter != null)
{
task.awaiter.UnsafeOnCompleted(continuation);
}
else
{
continuation();
}
}
ETTaskCompletionSource的相关代码,如果这个时候完成了,调用continuation,即状态机的moveNext。
void ICriticalNotifyCompletion.UnsafeOnCompleted(Action action)
{
this.continuation = action;
if (state != Pending)
{
TryInvokeContinuation();
}
}
private void TryInvokeContinuation()
{
this.continuation?.Invoke();
this.continuation = null;
}
this.requestCallback[rpcId] = (response) =>
{
if (ErrorCode.IsRpcNeedThrowException(response.Error))
{
tcs.SetException(new Exception($"Rpc Error: {request.GetType().FullName} {response.Error}"));
return;
}
tcs.SetResult(response);
};
ETTaskCompletionSource类中的代码
public void SetResult(T result)
{
if (this.TrySetResult(result))
{
return;
}
throw new InvalidOperationException("TaskT_TransitionToFinal_AlreadyCompleted");
}
public bool TrySetResult(T result)
{
if (this.state != Pending)
{
return false;
}
this.state = Succeeded;
this.value = result;
this.TryInvokeContinuation();
return true;
}
可以看到这部分就是await后面部分的代码,其中结果一层层调用最后就是ETTaskCompletionSource中的结果,之前已经设置过,所以可以获取了。
ETTaskCompletionSource中的获取结果
T IAwaiter<T>.GetResult()
{
switch (this.state)
{
case Succeeded:
return this.value;
case Faulted:
this.exception?.Throw();
this.exception = null;
return default;
case Canceled:
{
this.exception?.Throw(); // guranteed operation canceled exception.
this.exception = null;
throw new OperationCanceledException();
}
default:
throw new NotSupportedException("ETTask does not allow call GetResult directly when task not completed. Please use 'await'.");
}
}
public bool TrySetResult()
{
if (this.state != Pending)
{
return false;
}
this.state = Succeeded;
this.TryInvokeContinuation();
return true;
}
至此,整个C2R_LoginHandler的Run函数就走完了。
其他的ET异步比如:ETVoid,AsyncETVoidMethodBuilder等。主要区别就是在获取状态,public bool IsCompleted => true;
ETVoid里面的Awaiter直接设置为完成。当然如果不使用await ETVOID,直接调用,但是函数里面又要await其他Task,或者有直接return,不需要返回一个ETTASK来浪费,就可以使用ETVoid来兼容一些使用情况,或者你的异步不需要返回任何结果的,也可以用ETVoid。具体可以用反编译工具直接看下ETVOID的代码,这样能理解很多了。
简单来说,async,与await是编译器给我们的语法糖,主要用来用同步的方式,来写异步代码,避免了一直写回调的方式。
注意点:至于异步里面是用Task这种多线程的方式,还是ETTask这种类Task单线程方式,都是可以的。需要注意的是,Task在异步时,使用await会自动捕捉同步上下文,以及执行上下文,Task在构造时的代码,如果当前线程(Task.Run时)没有设置同步上下文ThreadSynchronizationContext,则await后的代码(看成Task.Oncomplete的回调),不会调用ThreadSynchronizationContext.post方法,则不会回到之前Task.run时的线程继续执行,同时执行上下文也可以设置不进行捕捉(这样执行上下文就不会流转了,异步本地变量会使用这个方式)。