首页 > 编程语言 >ET框架6.0分析二、异步编程

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

时间:2023-05-15 09:35:31浏览次数:36  
标签:异步 协程 C# 线程 6.0 ET public

概述

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

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

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

ETTask

C# 的异步函数有三个返回值(现在好像.NET7又多了一个ValueTask):Task,Task<T>,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<Action> queue = new ConcurrentQueue<Action>();

    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<T> cursor = await self.GetCollection<T>(collection).FindAsync(d => d.Id == id);

        return await cursor.FirstOrDefaultAsync();
    }
}

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

标签:异步,协程,C#,线程,6.0,ET,public
From: https://www.cnblogs.com/hggzhang/p/17206719.html

相关文章

  • 2023-05-15 leetcode周赛题
    找出转圈游戏输家mysolution100%passclassSolution:defcircularGameLosers(self,n:int,k:int)->List[int]:seen=set()now_num=1step=1seen.add(1)while1:stepSum=step*ktotal=now_num+stepSumnow_num=tot......
  • 常用设计模式之.Net示例代码合集
    每一次初学者粉丝朋友,在后台向我咨询编程问题,我除了给他们指导学习路线,我都会建议他们学完基础知识后,一定要要注重编程规范,学习设计模式,修炼内功。虽然说很多程序员,他们日常主要工作是CRUD,但是学习设计模式也是有助于学习公司的框架,另外设计模式是为了可重用代码、让代码更容易被......
  • 小议ml.NET机器学习与人机责任划分
    最近,特斯拉宣布召回110万辆车,名义上是纠正单踏板不良习惯,背后原因可能是这些车辆的电子控制单元存在缺陷,可能导致刹车失灵(潮州等交通事故至今没有定论)。这个事件引起了人们对于机器学习技术和人机责任划分的关注和讨论。机器学习技术在汽车制造业中的应用越来越广泛,可以帮助汽......
  • WebApplicationInitializer究 Spring 3.1之无web.xml式 基于代码配置的servlet3.0应用
    大家应该都已经知道Spring3.1对无web.xml式基于代码配置的servlet3.0应用。通过spring的api或是网络上高手们的博文,也一定很快就学会并且加到自己的应用中去了。PS:如果还没,也可以小小参考一下鄙人的上一篇文章<<探Spring3.1之无web.xml式基于代码配置的servlet3.0应用>>。    ......
  • 小知识:设置archive_lag_target参数强制日志切换
    为客户测试一个ADG场景问题,发现测试环境的日志切换频率过低,总是需要定期手工切换,这非常影响测试心情。实际上,可以设置archive_lag_target参数强制日志切换。比如设置:altersystemsetarchive_lag_target=1800;这样即使库没任何压力,半小时也会切换一次日志。该设置同时也适......
  • 王者荣耀吕布技能解析--- aggrandizement ,lunette ,lunette ,domian
    简单好用又强大的上单,稳定可靠被动饕餮血统,附魔强化后攻击补血---aggrandizement 强化前缀ab,ac,ad,af,ag,al,an,ap,ar,as,at-来自拉丁介词ad,表示“朝、向、去,或弱化为强调”。在字母b,f,g,l,n,p,r,s,t前同化为ab-,af-,ag-,al-,an-,ap-,ar-,as-,at-;在c......
  • .Net6创建grpc
    .NetCore(.Net6)创建grpc 1.环境要求.Net6,VisualStudio2019以上官方文档: https://learn.microsoft.com/zh-cn/aspnet/core/tutorials/grpc/grpc-startNetFramework版本: https://www.cnblogs.com/dennisdong/p/17119944.html2.搭建帮助类2.1新建类库GrpcCommon......
  • 关于 UE4 的 TSet
    TSet是一种快速容器类,(通常)用于在排序不重要的情况下存储唯一元素。TSet类似于TMap和TMultiMap,但有一个重要区别:TSet是通过对元素求值的可覆盖函数,使用数据值本身作为键,而不是将数据值与独立的键相关联。TSet可以非常快速地添加、查找和删除元素。默认情况下,TSet不支持重......
  • telnet命令无法使用?
     解决方法:安装telnet客户端控制面板-->程序-->程序和功能(appwiz.cpl)-->启用或关闭Windows功能-->功能-->添加功能-->telnet客户端-->安装......
  • 【LeetCode字符串#extra】KMP巩固练习:旋转字符串、字符串轮转
    旋转字符串https://leetcode.cn/problems/rotate-string/给定两个字符串,s和goal。如果在若干次旋转操作之后,s能变成goal,那么返回true。s的旋转操作就是将s最左边的字符移动到最右边。例如,若s='abcde',在旋转一次之后结果就是'bcdea'。示例1:输入:s="......