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> timers
,Queue<long> timeOutIds
,Queue<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,以及协程锁超时时间。
- 根据协程锁类型,获取CoroutineLockQueueType类,在CoroutineLockComponent(Entity)类初始化时,
List<CoroutineLockQueueType> list
已经根据枚举CoroutineLockType实例化完毕。 - 从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中猫大大部分都已经封装处理好了,不需要我们再写了。