• ET框架6.0分析二、异步编程


    概述

    ET框架很多地方都用到了异步,例如资源加载、AI、Actor模型等等。ET框架对C#的异步操作进行了一定程度的封装和改造,有一些特点:

    • 显式的或者说强调了使用C#异步实现协程机制(其实C#的异步编程天生就能实现这种用法)
    • 强制单线程异步
    • 没有使用C#库的Task,自己实现了ETTask等类
    • 实现了协程锁

    为了更好的理解下面的内容,推荐先看一下之前写的这两篇文章:

    ETTask

    C# 的异步函数有三个返回值(现在好像.NET7又多了一个ValueTask):Task,Task,void,对应的,ET框架也一样对应实现了:ETTask,ETTask/,ETVoid,其实现相比C#简化了一些逻辑,并添加一些新的特性以适应ET框架,其实使用起来是差不多的。为了实现ETTask,也实现了对应AsyncTaskCompletedMethodBuilder的AsyncETTaskCompletedMethodBuilder等类(其实还C#原来的逻辑差不太多,有兴趣可以看下上述C# 异步编程的链接)。

    ETTask添加了一些特性:

    • 支持对象池
    • 显式强调协程
    [DebuggerHidden]
    private async ETVoid InnerCoroutine()
    {
        await this;
    }
    
    [DebuggerHidden]
    public void Coroutine()
    {
        InnerCoroutine().Coroutine();
    }
    

    可以看到这里的所谓协程Coroutine,其实等效于 await task,只是平平无奇的异步调用罢了

    • 异常消息打印

    同步上线文 SynchronizationContext

    C#异步编程在大多数情况下会使用多线程,ET的异步操作例如定时器等,使用多线程的开销相比较大,且ET框架是多进程,性能是分摊到多个进程中。所以ET使用了单线程的异步。

    ThreadSynchronizationContext继承自SynchronizationContext,在构造初始化是会把自身设为当前SynchronizationContext.Current,重写了Post(异步消息分派到同步上下文)方法,来改写异步消息的分派到当前线程(就是进入队列)。

    而异步函数在执行时,会获取当前上下文(__builder.AwaitUnsafeOnCompleted方法会调用GetCompletionAction,内部调用ExecutionContext.FastCapture(),这个方法内部捕获SynchronizationContext,感兴趣可以关键词搜索下)

    public class ThreadSynchronizationContext : SynchronizationContext
    {
        // 线程同步队列,发送接收socket回调都放到该队列,由poll线程统一执行
        private readonly ConcurrentQueue queue = new ConcurrentQueue();
    
        private Action a;
    
        public void Update()
        {
            while (true)
            {
                if (!this.queue.TryDequeue(out a))
                {
                    return;
                }
    
                try
                {
                    a();
                }
                catch (Exception e)
                {
                    Log.Error(e);
                }
            }
        }
    
        public override void Post(SendOrPostCallback callback, object state)
        {
            this.Post(() => callback(state));
        }
        
        public void Post(Action action)
        {
            this.queue.Enqueue(action);
        }
    }
    
    public class MainThreadSynchronizationContext: Singleton<MainThreadSynchronizationContext>, ISingletonUpdate
    {
        private readonly ThreadSynchronizationContext threadSynchronizationContext = new ThreadSynchronizationContext();
    
        public MainThreadSynchronizationContext()
        {
            SynchronizationContext.SetSynchronizationContext(this.threadSynchronizationContext);
        }
        
        public void Update()
        {
            this.threadSynchronizationContext.Update();
        }
        
        public void Post(SendOrPostCallback callback, object state)
        {
            this.Post(() => callback(state));
        }
        
        public void Post(Action action)
        {
            this.threadSynchronizationContext.Post(action);
        }
    }
    
    // MainThreadSynchronizationContext.Instance.Update()
    Game.Update();
    

    ThreadSynchronizationContex由包裹的MainThreadSynchronizationContext驱动更新,MainThreadSynchronizationContext是个单件,由外面驱动。更新Update方法会把队列里的委托取出执行。

    SynchronizationContext

    假设有两个线程,一个UI线程,一个后台线程,一个业务先在后台线程计算数据,然后在UI线程中刷新显示数据,显然不同的线程其上下文环境是不同的,两个线程的通信可以使用SynchronizationContext完成。
    SynchronizationContext官方文档 https://learn.microsoft.com/zh-CN/dotnet/api/system.threading.synchronizationcontext?view=netcore-3.0

    协程锁

    多线程编程,对公共资源的访问要加锁,以保证数据访问的安全。类似的,在ET的异步编程中,从虽然上文中可以了解到ET的异步其实是单线程的,从代码运行的层面其实是一个线程以某种顺序处理一个个的任务,但是这种“顺序”并不可控。ET这里的协程锁其实就是使用某个key,对所有用这个key包裹的代码段推入一个队列,只有前面的代码段执行结束才能执行后面的代码。

    这看起来和C#平时用的lock(object),其实只是用法上比较像,其实在实现细节是有根本的差距的:简单来说。ET实现的协程锁是一种用户态的锁,不会造成内核态/用户态的切换。而lock是一种C#语法糖,在编译时其实是通过Monitor监视器实现的,会涉及到内核转换。一个线程上可能会运行成百上千个协程,如果这个线程被挂起,那么有可能造成很多协程Delay,可能造成灾难性的后果。

    结构类图:
    image
    时序图:
    image
    结合ET工程官方的一个用法:

    public static async ETTask<T> Query<T>(this DBComponent self, long id, string collection = null) where T : Entity
    {
        using (await CoroutineLockComponent.Instance.Wait(CoroutineLockType.DB, id % DBComponent.TaskCount))
        {
            IAsyncCursor cursor = await self.GetCollection(collection).FindAsync(d => d.Id == id);
    
            return await cursor.FirstOrDefaultAsync();
        }
    }
    

    可以看到协程锁是被using包裹的,即{}包裹的代码块运行结束,协程锁会被dispose。
    先来看当第一次调用Wait时会直接返回,当第一次的锁没有被dispose时,后面获取锁时会进入队列。当前面的锁被dispose时,会通知队列中后面一个锁在下一次Update时被Notify,SetResult获取到锁,其所属的代码段得以执行。

  • 相关阅读:
    Android 应用退出方式
    Javascript的form表单校验输入框
    [坚持打卡23天]力扣leetcode 面试题 01.08. 零矩阵
    手机开机入网流程 && KPI接通率和掉线率
    2023年中国金属涂胶板行业供需分析:销量同比增长2.8%[图]
    FFMPEG音视频开发指南(一)
    技术宅星云的Mac系统使用经验分享
    AI低代码,或将再次颠覆开发行业
    VMware安装Ubuntu20(图文教程,超详细)
    电脑上播放4K视频需要具备哪些条件?
  • 原文地址:https://www.cnblogs.com/hggzhang/p/17206719.html