消息队列的实现

news/2024/11/29 4:50:57/

【前言】

游戏的主逻辑一般是单线程的,所以实现一个消息队列很简单,不像互联网开发中会涉及多线程、多进程。可以先看看这篇文章。

对回调函数和消息机制的理解_消息回调函数_永恒星的博客-CSDN博客

这里尝试先去分析一些要素,然后直接基于这些要素去写代码,而不是边写边分析。

【分析1—基本功能】

由于消息队列需要被跨模块调用,它应该是一个静态类,不能被继承,不能用单例的形式。

对于消息发送者(Sender)而言,需要有一个SendMessage的方法,方法中的参数是发送的消息内容。

对于消息接受者(Receiver)而言,需要有一个AddListener方法去监听消息,对应的要有一个RemoveListener方法,显然,消息接受者需要传入一个回调函数。

消息队列中可能是任意形式的消息,因此消息内容需要用object类型来表示。这就需要对消息发送者发的任意形式的消息做一个封装。

消息队列中要管理消息,需要有个数据结构去缓存Message,这里直接用Queue。

消息列队需要将Message转发给Receiver,因此必须要持有Receiver,这里持有的是Receiver的回调函数,同时要有个数据结构去缓存,直接用List。

因为有多个Sender和Receiver,他们之间需要有区分,Receiver只接受指定类型的消息,但由于是跨模块调用的,不能事先预定义消息的类型,因此需要将消息类型的定义交给发送消息时具体的发送者去定义,也即在SendMessage时要传入一个消息类型的字段作为参数,这个参数可以是枚举类型的。

在添加了一个参数后,消息队列拿到Sender的Message时,除了要缓存消息内容Content,还需要缓存消息类型Type。因为Type一定时,Content不定,也即Sender可能发送同一类型的不同内容的消息,可以做个字典来缓存两者,即Dictionay<Type,Queue<Content>>。但这样处理的话破坏了按消息到来的顺序来发送消息的基本原理了。因此,为了标识Content是什么Type,需要将二者封装成一个新的数据类型。

将消息转发给Receiver时,可以根据IMessage里的Type,找到对应的Receiver,显然缓存接受者需要Dictionary。

到这里实现了基本的消息队列了,可以先将代码写出来,然后继续分析。代码如下:

using System.Collections;
using System.Collections.Generic;public class MessageWrapper
{public MessageType type;public object content;
}public enum MessageType
{None = 0,EnterGame = 1,ClickButton = 2,Kill = 3,//根据使用需要不断往下添加
}public delegate void MessageCb(object content);
public sealed class MessageQueue 
{private static Queue<MessageWrapper> messages = new Queue<MessageWrapper>();private static Dictionary<MessageType, MessageCb> listeners = new Dictionary<MessageType,MessageCb>();public static void SendMessage(MessageType type,object content){MessageWrapper messageWrapper = new MessageWrapper();messageWrapper.type = type;messageWrapper.content = content;messages.Enqueue(messageWrapper);}public static void AddListener(MessageType type, MessageCb cb){if(listeners.TryGetValue(type,out var messageCb)){listeners[type] = messageCb + cb;//委托链}else{listeners.Add(type, cb);}}public static void RemoveListener(MessageType type, MessageCb cb){if (listeners.TryGetValue(type, out var messageCb)){if(messageCb != null){messageCb -= cb;listeners[type] = messageCb;}            }}public static void Tick(){while(messages.Count > 0){var message = messages.Dequeue();//做好管理,这里一定不为空var listener = listeners[message.type];SendMessagerInternal(listener, message.content);}}private static void SendMessagerInternal(MessageCb listener, object content){if(listener != null){listener(content);}}
}

 【分析2—其他情况】

1.何时发送:这里将消息在下一帧发送,如果需要当前帧发送呢。尽管使用消息时默认是下一帧发送的,我们还是需要添加一下对这种情况的处理。这里用了bool变量即可,一个bool表示两种可能,刚好将需要立即发送和下一帧发送做了区分。

2.重复的Receiver:如果同一个Receiver 调用 AddListener多次,该如何处理呢。就目前的实现而言,无法分辨重复的Receiver。如果需要分辨的话,可以遍历委托链找到重复的Receiver,也可以先Remove再Add的方式规避重复的Receiver。在这里保持这样一个原则:对于错误的调用,希望可以纠错,而不是忽略它。所以采用遍历委托链的方式找到是哪个Receiver重复了。但遍历比较耗时,如果一个Message有数千个Receiver就很耗时。消息机制中,Sender不能直接知道有哪些Receiver,但消息队列可以知道,既然如此,可以让Receiver直接表明自己,而不是通过回调函数来表明。这里让Receiver在AddListener时传入一个标识自己的Name。消息队列需要额外缓存这个标识。

3.空的Receiver:AddListener和RemoveListener应该一一对应,但其他人在用的时候可能会忘记写了,这里直接把错误打印出来。

4.某些模块在发出消息或接受消息前需要有自定义的处理,需要增加一个钩子函数,供这些模块做自定义处理。

到这里处理了一些其他情况,还可能有更多的情况是没考虑到的,这些情况可以随着使用逐渐暴露出来,根据这些情况再做处理即可,这主要是经验的积累了。考虑了其他情况的后代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class MessageWrapper
{public MessageType type;public object content;public bool isSend;
}public enum MessageType
{None = 0,EnterGame = 1,ClickButton = 2,Kill = 3,//根据使用需要不断往下添加
}public delegate void MessageCb(object content);
public delegate void Hook(MessageWrapper message);
public sealed class MessageQueue 
{private static Queue<MessageWrapper> messages = new Queue<MessageWrapper>();private static Dictionary<MessageType, MessageCb> listenerCb = new Dictionary<MessageType,MessageCb>();private static Dictionary<MessageType,HashSet<string>> listenerNames = new Dictionary<MessageType,HashSet<string>>();private static Dictionary<string,Hook> name2hook = new Dictionary<string,Hook>();public static void SendMessage(MessageType type,object content,bool sync = false){MessageWrapper messageWrapper = new MessageWrapper();messageWrapper.type = type;messageWrapper.content = content;messageWrapper.isSend = false;if(sync){SendMessagerInternal(messageWrapper);}else{messages.Enqueue(messageWrapper);}}public static void AddListener(MessageType type, MessageCb cb,string name){if(cb == null){Debug.LogError("cb is null");return;}if(string.IsNullOrEmpty(name)){Debug.LogError("name is null or empty");}if(listenerCb.TryGetValue(type,out var messageCb)){if(listenerNames[type].Contains(name)){Debug.LogError($"receiver is repeated for {type}");}else{listenerCb[type] = messageCb + cb;//委托链   listenerNames[type].Add(name);}}else{listenerCb.Add(type, cb);listenerNames.Add(type, new HashSet<string> { name });}}public static void RemoveListener(MessageType type, MessageCb cb,string name){if (listenerCb.TryGetValue(type, out var messageCb)){if(listenerNames[type].Contains(name)){if (messageCb != null){messageCb -= cb;listenerCb[type] = messageCb;}listenerNames[type].Remove(name);}else{Debug.LogWarning($"there is no receiver for {type}");}    }}public static void AddHook(string name, Hook hook){if (!name2hook.ContainsKey(name)){name2hook.Add(name, hook);}else{Debug.LogWarning($"hook of {name} is existed");}}public static void RemoveHook(string name){if (name2hook.ContainsKey(name)){name2hook.Remove(name);}else{Debug.LogWarning($"hook of {name} is not existed");}}public static void Tick(){while(messages.Count > 0){var message = messages.Dequeue();//做好管理,这里可以省去一些判断           SendMessagerInternal(message);}}public static void Dispose(){messages.Clear();listenerCb.Clear();listenerNames.Clear();}private static void SendMessagerInternal(MessageWrapper message){foreach (var item in name2hook.Values){item.Invoke(message);}if(listenerCb.TryGetValue(message.type,out var listener)){if (listener != null && !message.isSend){listener(message.content);message.isSend = true;}else{listenerNames.Remove(message.type);Debug.LogError($"listener is null for {message.type}");}}else{Debug.LogWarning($"there is no receiver for {message.type}");}}}

【分析情况3——性能和内存】 

 1.message每次都是重新new,而消息队列作为一个基础模块,使用的频率会很高,每次重新new会的性能开销就不可忽略,同时也会引起内存碎片化,需要加个Pool来处理,这Pool直接放在其自身。Pool的初始容量的确定需要根据实际的使用情况来确定,统计下实际使用时的峰值来给定初始的容量。这里先随便给一个。

2.每个发送的消息在Invoke后,Receiver可能要进行很多处理,如果在每帧Invoke过多,那么可能会导致这一帧耗时过长,因此可以对每帧发送的消息做一个限制,也即分帧处理。具体每帧处理多少个需要结合实际项目来看,这里随便给一个。

3.消息的内容content是object类型,可能会发生装箱和拆箱,产生GC,需要修改成不会拆箱装箱的形式,方法就是添加一个新的泛型字段来缓存。同时,将MessageWapper传递给Receiver,Receiver使用这个字段来获取消息的内容。

考虑了性能和内存的情况如下:

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;public interface IMessage 
{MessageType type { get;set; }object content { get; set; }bool isSend { get; set; }void Release();}
public class MessageWrapper<TContent> : IMessage
{private MessageType _type;public MessageType type{get { return _type; }set { _type = value; }}private object _content;public object content{get { return _content; }set { _content = value; }}private bool _isSend;public bool isSend{get { return isSend; }set  {isSend = value;}}private TContent _ncontent;public TContent ncontent{get { return _ncontent; }set { _ncontent = value; }}private void Clear(){_content =null; _isSend = false;}public void Release(){Clear();Release(this);}private static Queue<MessageWrapper<TContent>> messagePool = new Queue<MessageWrapper<TContent>>(Enumerable.Range(0, 10).Select(i => new MessageWrapper<TContent>()));public static MessageWrapper<TContent> Allocate(){MessageWrapper<TContent> result = null;if (messagePool.Count > 0){result = messagePool.Dequeue();}else{result = new MessageWrapper<TContent>();}return result;}private static void Release(MessageWrapper<TContent> message){if (message == null) return;messagePool.Enqueue(message);}}public enum MessageType
{None = 0,EnterGame = 1,ClickButton = 2,Kill = 3,//根据使用需要不断往下添加
}public delegate void MessageCb(IMessage message);
public delegate void Hook(IMessage message);
public sealed class MessageQueue 
{private static readonly int MAX_COUNT_MESSAGE = 20;private static Queue<IMessage> messages = new Queue<IMessage>();private static Dictionary<MessageType, MessageCb> listenerCb = new Dictionary<MessageType,MessageCb>();private static Dictionary<MessageType,HashSet<string>> listenerNames = new Dictionary<MessageType,HashSet<string>>();private static Dictionary<string,Hook> name2hook = new Dictionary<string,Hook>();public static void SendMessage(MessageType type,object content = null,bool sync = false){MessageWrapper<object> messageWrapper = MessageWrapper<object>.Allocate();messageWrapper.type = type;messageWrapper.content = content;messageWrapper.isSend = false;if(sync){SendMessagerInternal(messageWrapper);curCount++;}else{messages.Enqueue(messageWrapper);}}public static void SendMessage<TContent>(MessageType type, TContent content = default, bool sync = false){MessageWrapper<TContent> messageWrapper = MessageWrapper<TContent>.Allocate();messageWrapper.type = type;messageWrapper.ncontent = content;messageWrapper.isSend = false;if (sync){SendMessagerInternal(messageWrapper);curCount++;}else{messages.Enqueue(messageWrapper);}}public static void AddListener(MessageType type, MessageCb cb,string name){if(cb == null){Debug.LogError("cb is null");return;}if(string.IsNullOrEmpty(name)){Debug.LogError("name is null or empty");}if(listenerCb.TryGetValue(type,out var messageCb)){if(listenerNames[type].Contains(name)){Debug.LogError($"receiver is repeated for {type}");}else{listenerCb[type] = messageCb + cb;//委托链   listenerNames[type].Add(name);}}else{listenerCb.Add(type, cb);listenerNames.Add(type, new HashSet<string> { name });}}public static void RemoveListener(MessageType type, MessageCb cb,string name){if (listenerCb.TryGetValue(type, out var messageCb)){if(listenerNames[type].Contains(name)){if (messageCb != null){messageCb -= cb;listenerCb[type] = messageCb;}listenerNames[type].Remove(name);}else{Debug.LogWarning($"there is no receiver for {type}");}    }}public static void AddHook(string name, Hook hook){if (!name2hook.ContainsKey(name)){name2hook.Add(name, hook);}else{Debug.LogWarning($"hook of {name} is existed");}}public static void RemoveHook(string name){if (name2hook.ContainsKey(name)){name2hook.Remove(name);}else{Debug.LogWarning($"hook of {name} is not existed");}}private static int curCount = 0;public static void Tick(){while(messages.Count > 0){curCount++;if (curCount > MAX_COUNT_MESSAGE){curCount = 0;break;}var message = messages.Dequeue();//做好管理,这里可以省去一些判断           SendMessagerInternal(message);}}public static void Dispose(){foreach (var item in messages){item.Release();}messages.Clear();listenerCb.Clear();listenerNames.Clear();}private static void SendMessagerInternal(IMessage message){foreach (var item in name2hook.Values){item.Invoke(message);}if(listenerCb.TryGetValue(message.type,out var listener)){if (listener != null && !message.isSend){message.isSend = true;listener(message);}else{listenerNames.Remove(message.type);Debug.LogError($"listener is null for {message.type}");}}else{Debug.LogWarning($"there is no receiver for {message.type}");}message.Release();}}

【总结】

 对于实现一个消息队列,首先实现其基本功能,其次考虑各种不同的使用情况,在基本功能的基础上做额外的修改,最好考虑性能和内存,再做优化。


http://www.ppmy.cn/news/64870.html

相关文章

如何从小白成长为一名运维专家

运维是系统管理和维护的一部分&#xff0c;要从小白成长为一名运维专家&#xff0c;需要不断学习、实践和积累经验。以下是一些建议&#xff0c;可以帮助您从小白成长为运维专家。 1、学习基础知识&#xff1a;掌握计算机基础知识&#xff0c;如操作系统(如Linux、Windows等)和…

2023 年第八届数维杯数学建模挑战赛 赛题浅析

为了更好地让大家本次数维杯比赛选题&#xff0c;我将对本次比赛的题目进行简要浅析。本次比赛的选题中&#xff0c;研究生、本科组请从A、B题中任选一个 完成答卷&#xff0c;专科组请从B、C题中任选一个完成答卷。这也暗示了本次比赛的难度为A>B>C 选题人数初步估计也…

多尺度深度特征(上):多尺度特征学习才是目标检测精髓(干货满满,建议收藏)...

计算机视觉研究院专栏 作者&#xff1a;Edison_G 深度特征学习方案将重点从具有细节的具体特征转移到具有语义信息的抽象特征。它通过构建多尺度深度特征学习网络 (MDFN) 不仅考虑单个对象和局部上下文&#xff0c;还考虑它们之间的关系。 公众号ID&#xff5c;ComputerVisionG…

Cadence技巧总结学习(DRC、Annotate)持续更新~

Cadence技巧总结学习持续更新~ 你还可以再哪里看到这篇文章&#xff1a;知乎 1. 画叉 对于芯片上不用的引脚信号画上号&#xff0c;如下&#xff1a; 按大写X就可以了&#xff0c;或是双脚引脚&#xff0c;在跳出的界面中&#xff0c;Is No Connect上✔。 2. 画线快捷键&#…

IPC:匿名管道和命名管道

一 管道初级测试 写两个小程序&#xff0c;一个负责向管道发数据&#xff0c;一个从管道接收数据&#xff1b; pipe.cpp #include <iostream> using namespace std;int main() {cout << "hello world" << endl;return 0; } pipe2.cpp #inclu…

C++类和对象(6)

类和对象 1.在谈构造函数1.1. 构造函数体赋值1.2. 初始化列表1.3. explicit关键字 2. static成员2.1. 概念2.2. 特性 3.友元函数3.2.友元类 4. 内部类5.匿名对象6.拷贝对象时的一些编译器优化7.再次理解类和对象 1.在谈构造函数 1.1. 构造函数体赋值 在创建对象时&#xff0c…

在Linux系统中搭建Docker环境

搭建Docker环境 文章目录 搭建Docker环境Ubuntu版本安装DockerCentos版本安装Docker配置镜像加速 Ubuntu版本安装Docker 按照以下步骤在 Ubuntu 上安装 Docker&#xff1a; 卸载旧版本的 Docker&#xff08;如果有&#xff09;&#xff1a; sudo apt-get remove docker docker…

云开发谁是卧底线下小游戏发牌助手微信小程序源码

源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/87614365 云开发谁是卧底线下小游戏源码&#xff0c;发牌助手微信小程序源码。 “谁是卧底OL”是一个非常有趣&#xff0c;风靡全国的比拼语言表述能力、知识面与想象力的游戏。 谁是卧底OL是一款由开发…