ET服务器框架学习笔记-杂记(ETTask,async,await)

ET服务器框架学习笔记-杂记(ETTask,async,await)

这篇文章主要解释ET服务器框架中的ETTak相关,异步相关,async与await相关


文章目录

  • ET服务器框架学习笔记-杂记(ETTask,async,await)
  • 一、C#中的async与await
  • 二、ETTask相关异步
    • 2.解读这段代码
    • 3.其他的处理
  • 总结


一、C#中的async与await

首先要说的是,async与await是C#编译器给的一种语法糖。使得大家可以以一种同步写代码的方式,实现异步处理。简单的来说,就是我现在有两个任务,1.开洗衣机洗衣服,2.我要吃饭。所以正常方式是, 我开洗衣机,等它洗完衣服,就把衣服拿出来晒,晒完了,去吃饭,这就是同步。而聪明的人肯定是,我开洗衣机,然后去吃饭,等衣服洗完了,洗衣机会发出完成的声音,告诉我该停下吃饭,继续把衣服拿出来晒了,晒完了可以继续吃饭。这样大大节约了时间。
回调是一种将方法当作参数传给其他方法函数使用,并在合适时机进行调用的机制。这种回调就相当于上面洗衣机洗完了,告诉我。
而平时可能回调一两次,我们觉得很简单,但是如果多了,那就是回调地狱了。曾经维护过一个界面关闭的各种回调,各种恶心。现在C#提供了async与await的语法糖,帮助开发者大大简化了开发异步的写法。类似下面这种伪代码:
定义方法:async 洗衣服() //这里可能需要半个小时
定义方法:晒衣服()
定义方法:吃饭()
定义方法:搞家务(){await 洗衣服;晒衣服}
主方法:干活{搞家务(),吃饭()}
上面干活的时候,开了洗衣机就马上可以吃饭了,洗衣服可能需要半个小时,半个小时后就直接接着搞家务洗衣服了。不需要把吃饭的回调放到搞家务里面,然后洗衣机开了,回调吃饭,然后洗完衣服回调晒衣服了。

不知道上面的例子讲清楚没有。。。姑且就这么看着吧,下面用实际代码来说下ETTask的相关内容。

二、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,咱们用反编译工具来看下编译器给我们生成的代码如下:

ET服务器框架学习笔记-杂记(ETTask,async,await)_第1张图片
可以看到里面帮我们生成了一个状态机d__0类,继承了IAsyncStateMachine,里面把之前的原始定义的字段与属性,都取了另外的名字,核心代码:MoveNext(),打开看到如下:
ET服务器框架学习笔记-杂记(ETTask,async,await)_第2张图片

2.解读这段代码

这段代码确实看起来很可怕,各种乱七八糟的变量名,过程也很长,但是不要怕,咱们只看关键流程就好。

  1. 初始化状态机的builder,这里是AsyncETTaskMethodBuilder.Create(),为啥是这个呢,我们看Session.Call代码:
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 Task => new ETTask(this);

public ETTask(IAwaiter<T> awaiter)
        {
            this.result = default;
            this.awaiter = awaiter;
        }

这里就把上面的ETTaskCompletionSource传给ETTask的awaiter了,(要记住这一点,因为好几个awaiter在里面。)
这个ETTask类定义的时候加了ET服务器框架学习笔记-杂记(ETTask,async,await)_第3张图片
这里就表明了,这个ETTask生成状态机时,里面的builder用的AsyncETTaskMethodBuilder这个类。

  1. 将一些变量从主函数中获取,并设置到状态机里面,这就不多说了。将状态机初始状态设置为-1。
    调用builder的start方法,并传入状态机,返回ETTASK给外层。
    ET服务器框架学习笔记-杂记(ETTask,async,await)_第4张图片
  2. 接着看builder的start方法里面的代码:
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
        {
            stateMachine.MoveNext();
        }

很简单就是调用传入的状态机的moveNext方法。

  1. 查看moveNext方法,内部就是几个状态切换,主要就是查看状态值,根据状态值,做不同的处理。我们分为几块来说下:
  2. 当状态不为0时,执行原代码await之前的语句:
    ET服务器框架学习笔记-杂记(ETTask,async,await)_第5张图片
    其中最关键的就是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 Task => new ETTask(this);

  1. 根据实例化的Awaiter对象查看IsCompleted,就是查看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状态是否完成。

  1. 如果没有完成,则把状态机的状态设置为0(一开始是-1),初始化状态的awaiter,然后调用builder的AwaitUnsafeOnCompleted方法,传入awaiter与状态机。
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;
        }
  1. 第一步就这么完成了,然后一直等着有人去设置结果,即ETTaskCompletionSource的SetResult,在这个例子就是下面这段代码,即有消息回来。
this.requestCallback[rpcId] = (response) =>
			{
				if (ErrorCode.IsRpcNeedThrowException(response.Error))
				{
					tcs.SetException(new Exception($"Rpc Error: {request.GetType().FullName} {response.Error}"));
					return;
				}

				tcs.SetResult(response);
			};
  1. 接着看设置结果发生了什么,首先调用TrySetResult,内部设置ETTaskCompletionSource类的状态为完成,回到了continuation的调用,即回到最初的状态机MoveNext调用。

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;

        }
  1. 由于之前第一步等待的时候,已经设置状态机的状态为0了,所以执行下面这段代码

ET服务器框架学习笔记-杂记(ETTask,async,await)_第6张图片
可以看到这部分就是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'.");
            }
        }
  1. 后续的代码就是,捕捉异常,与清理状态机的一些本地变量,最后一句SetResult(),最终会回到ETTaskCompletionSource里面去,由于状态已经是Succeeded,所以就直接return false了。
public bool TrySetResult()
        {
            if (this.state != Pending)
            {
                return false;
            }

            this.state = Succeeded;

            this.TryInvokeContinuation();
            return true;

        }

至此,整个C2R_LoginHandler的Run函数就走完了。

3.其他的处理

其他的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时的线程继续执行,同时执行上下文也可以设置不进行捕捉(这样执行上下文就不会流转了,异步本地变量会使用这个方式)。

你可能感兴趣的:(ET,c#)