“对消息或事件的发送与受理进行时间上的解耦。”
我们经常听到 消息队列、事件循环、消息泵。
事件队列是一个按照先进先出顺序存储一系列通知或请求的队列。发出通知时系统会将该请求置入队列并随即返回,请求处理器随后从事件队列中获取并处理这些请求。请求可由处理器直接处理或转交给对其感兴趣的模块。这一模式对消息的发送者与受理者进行了解耦,使消息的处理变得动态且非实时。
A想让B做某些事,A将事件塞入事件队列,然后事件读时处理时,执行事件。这样A就不会卡住了。
但是,如果A发事件,然后目标会同步直接处理,可能会造成循环问题,例如
但将事件缓存到某个地方,然后下一帧去读,然后处理,能解决这种问题。
或者,一个常用的规避法则是避免在处理事件端代码中发送事件。
下面的是一个简单样例
class Audio
{
public:
static void init() { numPending_ = 0; }
// Other stuff...
private:
static const int MAX_PENDING = 16;
static PlayMessage pending_[MAX_PENDING];
static int numPending_;
};
void Audio::playSound(SoundId id, int volume)
{
assert(numPending_ < MAX_PENDING);
pending_[numPending_].id = id;
pending_[numPending_].volume = volume;
numPending_++;
}
class Audio
{
public:
// 在更新方法中处理事件
static void update()
{
for (int i = 0; i < numPending_; i++)
{
ResourceId resource = loadSound(
pending_[i].id);
int channel = findOpenChannel();
if (channel == -1) return;
startSound(resource, channel,
pending_[i].volume);
}
numPending_ = 0;
}
// Other stuff...
};环状缓冲区保有数组所有的有点,同时允许我们从队列的前端持续地移除元素。
就像这样
class Audio
{
public:
static void init()
{
head_ = 0;
tail_ = 0;
}
// Methods...
private:
static int head_;
static int tail_;
// Array...
};加入队列
void Audio::playSound(SoundId id, int volume)
{
assert((tail_ + 1) % MAX_PENDING != head_);
// Add to the end of the list.
pending_[tail_].id = id;
pending_[tail_].volume = volume;
tail_ = (tail_ + 1) % MAX_PENDING;
}更新方法
void Audio::update()
{
//If there are no pending requests, do nothing.
if (head_ == tail_) return;
ResourceId resource = loadSound(
pending_[head_].id);
int channel = findOpenChannel();
if (channel == -1) return;
startSound(resource, channel,
pending_[head_].volume);
head_ = (head_ + 1) % MAX_PENDING;
}如果最大容量会有问题,你可以使用可增长的数组。当队列满了以后,分配一个新的数组,大小是当前数组的二倍(或其他的倍数),并把原数组中的项拷贝过去。
即使在数组增长的时候拷贝,入列一个元素仍然有常量级的复杂度。
Go编程语言内置的“通道”类型,本质就是一个事件队列或消息队列。