ET服务器框架学习笔记-6.0杂记(异步协程锁)

20 篇文章 8 订阅

ET服务器框架学习笔记-6.0杂记(异步协程锁)

为了解决,使用异步编程时,某些情况下,需要异步按照我们编写的顺序执行。
例如:查询同一个玩家的数据,场景:玩家登录->查询玩家身上金币->给玩家增加挂机金币,这时候由于是查询数据库操作,使用了异步,等待查询完毕,再执行计算金币。这个时候程序执行其他操作,比如登录扣除玩家金币,也是要查询玩家金币,又是查询数据库操作,又是异步。这个时候,没有协程锁会带来两个问题:1.都去数据库查玩家数据,带来性能浪费,只需要一个查询好已经通知到内存缓存了,另外一个可以直接读出数据来;2.后一个异步可能会先到,先扣了金币,可能会金币不足扣除失败(而设计要求是一定要先加挂机金币,再扣除)。


总之,协程锁可以解决异步带来的执行顺序问题,并可以在某些情景下提升性能


一、相关类与数据结构

CoroutineLock:协程锁,获得这个类即代表获得了针对某个key的对象使用权,可以继续执行。
CoroutineLockInfo:包含一个ETTask<CoroutineLock> Tcs;一个表示超时时间的Time。其中TCS用于其他类获取协程锁时,用于异步等待的类Task对象。
CoroutineLockQueue:内部封装一个Queue<CoroutineLockInfo>,用于对同一个key的多个协程信息对象,且可以实现先进先出。
CoroutineLockQueueType:内部封装一个Dictionary<long, CoroutineLockQueue> dictionary用于针对每一个key,一个协程锁队列,方便查找
CoroutineLockComponent:主要封装一个List<CoroutineLockQueueType> list按照CoroutineLockType类型,对应存放每一个CoroutineLockQueueType。内部还增加了用于协程超时的各种超时相关的数据结构:MultiMap<long, CoroutineLockTimer> timersQueue<long> timeOutIdsQueue<CoroutineLockTimer> timerOutTimer

二、协程锁流程

1. 获取锁的流程,调用Wait,请求获取协程锁CoroutineLock,注意请求的地方一定要用using块包围(或者使用完对象后,手动dispose也行):

public static async ETTask<CoroutineLock> Wait(this CoroutineLockComponent self, CoroutineLockType coroutineLockType, long key, int time = 60000)
        {
            CoroutineLockQueueType coroutineLockQueueType = self.list[(int) coroutineLockType];
   
            if (!coroutineLockQueueType.TryGetValue(key, out CoroutineLockQueue queue))
            {
                coroutineLockQueueType.Add(key, EntityFactory.CreateWithId<CoroutineLockQueue>(self.Domain, ++self.idGenerator, true));
                return self.CreateCoroutineLock(coroutineLockType, key, time, 1);
            }

            ETTask<CoroutineLock> tcs = ETTask<CoroutineLock>.Create(true);
            queue.Add(tcs, time);
            
            return await tcs;
        }

主要功能:通过传入的协程锁类型,针对获取锁的key,以及协程锁超时时间。

  1. 根据协程锁类型,获取CoroutineLockQueueType类,在CoroutineLockComponent(Entity)类初始化时,List<CoroutineLockQueueType> list已经根据枚举CoroutineLockType实例化完毕。
  2. 从CoroutineLockQueueType类中获取针对key的CoroutineLockQueue类,此时分支处理。(a).如果没有这个类,则创建一个CoroutineLockQueue实体,并存放CoroutineLockQueueType对应的key-value字典中,说明针对这个key没有任何异步在处理,所以我们直接创建一个CoroutineLock 类,返回给使用的地方,让之前请求锁的地方可以直接往下运行;
public static CoroutineLock CreateCoroutineLock(this CoroutineLockComponent self, CoroutineLockType coroutineLockType, long key, int time, int count)
        {
            CoroutineLock coroutineLock = EntityFactory.CreateWithId<CoroutineLock, CoroutineLockType, long, int>(self.Domain, ++self.idGenerator, coroutineLockType, key, count, true);
            if (time > 0)
            {
                self.AddTimer(TimeHelper.ClientFrameTime() + time, coroutineLock);
            }
            return coroutineLock;
        }

(b).如果已经存在key对应的CoroutineLockQueue,则说明之前已经有异步针对这个key请求过至少一个锁(且还没有释放),创建一个ETTask<CoroutineLock>,通过CoroutineLockQueue的Add方法内部创建一个协程锁信息CoroutineLockInfo对象加入到对应的CoroutineLockQueue队列中,让请求的地方停止往下运行(即await后面的代码会等待异步完成,异步相关请看之前的文章)。

2.释放锁的流程, 在使用的地方,如下:

using (await CoroutineLockComponent.Instance.Wait(CoroutineLockType.**, **))
{
	//============各类处理
}

如果使用完毕,由于using的特性,会调用获取到的CoroutineLock的dispose,其中最关键的部分是EventSystem.Instance.Destroy(this);this.InstanceId = 0;这两个会在协程锁中使用。

public override void Dispose()
        {
            if (this.IsDisposed)
            {
                return;
            }

            EventSystem.Instance.Remove(this.InstanceId);
            this.InstanceId = 0;

            // 清理Component
            if (this.components != null)
            {
                foreach (KeyValuePair<Type, Entity> kv in this.components)
                {
                    kv.Value.Dispose();
                }

                this.components.Clear();
                dictPool.Recycle(this.components);
                this.components = null;

                // 从池中创建的才需要回到池中,从db中不需要回收
                if (this.componentsDB != null)
                {
                    this.componentsDB.Clear();

                    if (this.IsFromPool)
                    {
                        hashSetPool.Recycle(this.componentsDB);
                        this.componentsDB = null;
                    }
                }
            }

            // 清理Children
            if (this.children != null)
            {
                foreach (Entity child in this.children.Values)
                {
                    child.Dispose();
                }

                this.children.Clear();
                childrenPool.Recycle(this.children);
                this.children = null;

                if (this.childrenDB != null)
                {
                    this.childrenDB.Clear();
                    // 从池中创建的才需要回到池中,从db中不需要回收
                    if (this.IsFromPool)
                    {
                        hashSetPool.Recycle(this.childrenDB);
                        this.childrenDB = null;
                    }
                }
            }

            // 触发Destroy事件
            EventSystem.Instance.Destroy(this);

            this.domain = null;

            if (this.parent != null && !this.parent.IsDisposed)
            {
                if (this.IsComponent)
                {
                    this.parent.RemoveComponent(this);
                }
                else
                {
                    this.parent.RemoveChild(this);
                }
            }

            this.parent = null;

            if (this.IsFromPool)
            {
                ObjectPool.Instance.Recycle(this);
            }
            else
            {
                base.Dispose();
            }

            status = EntityStatus.None;
        }

调用了dispose后,InstanceId 变为0,且会调用对应的CoroutineLock的Destory。

public override void Destroy(CoroutineLock self)
        {
            if (self.coroutineLockType != CoroutineLockType.None)
            {
                CoroutineLockComponent.Instance.Notify(self.coroutineLockType, self.key, self.count + 1);
            }
            else
            {
                // CoroutineLockType.None说明协程锁超时了
                Log.Error($"coroutine lock timeout: {self.coroutineLockType} {self.key} {self.count}");
            }
            self.coroutineLockType = CoroutineLockType.None;
            self.key = 0;
            self.count = 0;
        }

self.coroutineLockType != CoroutineLockType.None时,会调用CoroutineLockComponent.Instance.Notify函数,如下:

public static void Notify(this CoroutineLockComponent self, CoroutineLockType coroutineLockType, long key, int count)
        {
            CoroutineLockQueueType coroutineLockQueueType = self.list[(int) coroutineLockType];
            if (!coroutineLockQueueType.TryGetValue(key, out CoroutineLockQueue queue))
            {
                return;
            }

            if (queue.Count == 0)
            {
                coroutineLockQueueType.Remove(key);
                return;
            }
            
#if NOT_UNITY
            const int frameCoroutineCount = 5;
#else
            const int frameCoroutineCount = 10;
#endif

            if (count > frameCoroutineCount)
            {
                self.NextFrameRun(coroutineLockType, key);
                return;
            }
            
            CoroutineLockInfo coroutineLockInfo = queue.Dequeue();
            coroutineLockInfo.Tcs.SetResult(self.CreateCoroutineLock(coroutineLockType, key, coroutineLockInfo.Time, count));
        }

主要功能:1.如果对应的queue中没有其他人再请求了,则直接在CoroutineLockQueueType中删除这个key(即对应的CoroutineLockQueue释放了),这样后续又有请求这个key对应锁时,会发现对应的CoroutineLockQueue没有,可以直接获取锁了,(参考上面的获取锁的流程)。2.如果队列中还有其他请求过这个协程锁对应的key的协程锁,则从队列中拿出对应的协程锁信息类CoroutineLockInfo,然后新建一个协程锁对象,并设置CoroutineLockInfo内部对应的ETTASK的Tcs.SetResult,让之前请求锁的异步继续执行。这样就是释放锁,让下一个等待相同key值的协程继续往下运行了。

3.超时的流程:每次CreateCoroutineLock在构建一个协程锁的同时,如果传入的等待时间>0,则会构造一个CoroutineLockTimer类放入CoroutineLockComponent的timers中,用于超时处理。

public static CoroutineLock CreateCoroutineLock(this CoroutineLockComponent self, CoroutineLockType coroutineLockType, long key, int time, int count)
        {
            CoroutineLock coroutineLock = EntityFactory.CreateWithId<CoroutineLock, CoroutineLockType, long, int>(self.Domain, ++self.idGenerator, coroutineLockType, key, count, true);
            if (time > 0)
            {
                self.AddTimer(TimeHelper.ClientFrameTime() + time, coroutineLock);
            }
            return coroutineLock;
        }
        
public static void AddTimer(this CoroutineLockComponent self, long tillTime, CoroutineLock coroutineLock)
        {
            self.timers.Add(tillTime, new CoroutineLockTimer(coroutineLock));
            if (tillTime < self.minTime)
            {
                self.minTime = tillTime;
            }
        }

在CoroutineLockComponent的Update方法(每帧运行),中检查是否有超时的(就算之前释放了的锁,也会在这个里面,会有相应处理)

public void TimeoutCheck(CoroutineLockComponent self)
        {
            // 超时的锁
            if (self.timers.Count == 0)
            {
                return;
            }

            long timeNow = TimeHelper.ClientFrameTime();

            if (timeNow < self.minTime)
            {
                return;
            }

            foreach (KeyValuePair<long, List<CoroutineLockTimer>> kv in self.timers)
            {
                long k = kv.Key;
                if (k > timeNow)
                {
                    self.minTime = k;
                    break;
                }

                self.timeOutIds.Enqueue(k);
            }
            
            self.timerOutTimer.Clear();
            
            while (self.timeOutIds.Count > 0)
            {
                long time = self.timeOutIds.Dequeue();
                foreach (CoroutineLockTimer coroutineLockTimer in self.timers[time])
                {
                    self.timerOutTimer.Enqueue(coroutineLockTimer);
                }
                self.timers.Remove(time);
            }
            
            while (self.timerOutTimer.Count > 0)
            {
                CoroutineLockTimer coroutineLockTimer = self.timerOutTimer.Dequeue();
                if (coroutineLockTimer.CoroutineLockInstanceId != coroutineLockTimer.CoroutineLock.InstanceId)
                {
                    continue;
                }

                CoroutineLock coroutineLock = coroutineLockTimer.CoroutineLock;
                // 超时直接调用下一个锁
                self.NextFrameRun(coroutineLock.coroutineLockType, coroutineLock.key);
                coroutineLock.coroutineLockType = CoroutineLockType.None; // 上面调用了下一个, dispose不再调用
            }
        }

将到时的coroutineLockTimer类找到,放入到nextFrameRun队列中等待处理,注意代码coroutineLockTimer.CoroutineLockInstanceId != coroutineLockTimer.CoroutineLock.InstanceId通过dispose释放了锁的代码,这里的coroutineLockTimer.CoroutineLock.InstanceId=0,所以不相等,通过dispose释放的锁不会进入处理中,且超时已经从timers中移除了。

public override void Update(CoroutineLockComponent self)
        {
            // 检测超时的CoroutineLock
            TimeoutCheck(self);
            
            int count = self.nextFrameRun.Count;
            // 注意这里不能将this.nextFrameRun.Count 放到for循环中,因为循环过程中会有对象继续加入队列
            for (int i = 0; i < count; ++i)
            {
                (CoroutineLockType coroutineLockType, long key) = self.nextFrameRun.Dequeue();
                self.Notify(coroutineLockType, key, 0);
            }
        }

处理超时的协程锁(这里的nextFrameRun中的都是超时没有处理完的协程锁),将会强行Notify,让队列中的下一个协程获得锁,从而继续执行。注意coroutineLock.coroutineLockType = CoroutineLockType.None,在CoroutineLock的destory中会用到。

		if (self.coroutineLockType != CoroutineLockType.None)
            {
                CoroutineLockComponent.Instance.Notify(self.coroutineLockType, self.key, self.count + 1);
            }
            else
            {
                // CoroutineLockType.None说明协程锁超时了
                Log.Error($"coroutine lock timeout: {self.coroutineLockType} {self.key} {self.count}");
            }

如果一个锁在dispose中释放时,发现coroutineLockType值为CoroutineLockType.None,即在超时中设置了,说明获取这个锁的异步执行了太久了,已经在超时了,很可能破坏了协程执行的顺序。

4.分帧处理流程

在Notify中有一段分帧处理的流程,如下:

#if NOT_UNITY
            const int frameCoroutineCount = 5;
#else
            const int frameCoroutineCount = 10;
#endif

            if (count > frameCoroutineCount)
            {
                self.NextFrameRun(coroutineLockType, key);
                return;
            }

针对普通释放锁,获取锁时,会增加锁的count数量,增加到一定次数后,再释放时,会判断count值大于某值时,将阻止这次释放锁从而让下一个协程获取锁的流程。而是将其视作为超时,将其放入到nextFrameRun中,让下一帧再来处理,即让下一个本该本帧获取到锁的并执行流程的逻辑,到下一帧才获取锁来执行逻辑,超时释放锁会让count归0。避免了某帧处理过多的锁,导致影响其他逻辑运行,进而提升了表现性能。

总结

在ET6.0中的异步协程锁,设计的相当巧妙,适用于相当多的情景了。基本上在ET6.0的枚举中,已经列出来了:

public enum CoroutineLockType
    {
        None = 0,
        Location,                  // location进程上使用
        ActorLocationSender,       // ActorLocationSender中队列消息 
        Mailbox,                   // Mailbox中队列
        UnitId,                    // Map服务器上线下线时使用
        DB,
        Resources,

        Max, // 这个必须在最后
    }

分别对应以下使用情景:多个向location查询同一个实体真实进程号地址(在访问跨进程实体时),访问一次获得进程地址即可 ;多个针对同一个实体对象,发起的Actor消息;多个处理针对同一个实体的Mailbox消息处理,处理需要按照先后顺序;针对Map中单位上下线时,新上线消息需要等待下线消息执行完后再处理;针对同一个DB访问的前后顺序处理; 多个资源请求同一个ab包下载的处理。如果还有自己想要处理的协程锁类型,可自行添加,不过现在这些应该已经够用了,且ET6.0中猫大大部分都已经封装处理好了,不需要我们再写了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值