15_游戏编程模式EventQueue
#### 两个例子 1.GUI event loop ``` while (running) { // 从事件队列里获取一个事件 Event event = getNextEvent(); // Handle event... } ``` 2.Central event bus 不同系统公用的通信中心 #### 有问题的code ``` class Audio { public: static void playSound(SoundId id, int volume); }; class Audio { public: static void playSound(SoundId id, int volume); }; class Menu { public: void onSelect(int index) { Audio::playSound(SOUND_BLOOP, VOL_MAX); // Other stuff... } }; ``` 问题: 1. api同步调用, 阻塞到audio处理完请求 2. 多个请求不能合并处理 3. 处理请求有可能运行在错误的线程上(没有锁) ####模式定义: ``` 一系列的通知或请求存储在先进先出的队列里. 发送通知进行入队; 请求处理者从队列里获取请求. ``` ##### 何时使用 ``` 1.如果只是想从sender那里获取消息,使用 observer 或者command将会更简单. 2.当你需要push什么到另一个模块,或者pull什么从另一个地方的时候, 你需要一个buffer, 此时就需要一个队列了. 3. 队里提供的pull操作, receiver可以延迟处理,合并请求, 或者丢弃. pull请求不开放给sender使用,当sender需要获得响应的时候,队列就有点技穷了.(send then pray) ``` #### 注意事项 ``` 1.中心事件队列是个全局变量 2.世界状态会改变,(队列处理不是及时的) 3.困在反馈循环里(a ->b -> a ->b ...).如果是同步队列的,你会很快的发现循环bug. 一般原则: 避免在在处理事件的函数里发送事件. ``` #### Sample Code ``` struct PlayMessage { SoundId id; int volume; }; 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... }; ``` ##### ring buffer 循环buffer ``` 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; } ``` ##### 合并请求 ``` void Audio::playSound(SoundId id, int volume) { // Walk the pending requests. for (int i = head_; i != tail_; i = (i + 1) % MAX_PENDING) { if (pending_[i].id == id) { // Use the larger of the two volumes. pending_[i].volume = max(volume, pending_[i].volume); // Don't need to enqueue. return; } } // Previous code... } ``` ##### 多线程 push pull操作需要线程安全 #### 队列里保存的是什么 ``` event or message 1 event queue(一对多) 描述一些已经发生的事情, 类似异步的observer模式 1.允许多个监听者, 队列里保存的都是*已经发生的事件*, 发送者不关心谁去接受它. 2.作用域更广.被用于广播一类的事情.趋向于全局可见. 2 message queue(多对一) 更趋向于只有一个监听者. 多个请求从不同的地方发来,一个处理者进行处理 ``` #### 谁可以读队列 ``` 1 单播队列: 1.队列实现读取. 发送者只管发送 2.队列被封装的更好 3.没有读取竞争(决定是广播还是挨个分配) 2 广播队列: 1.如果没有监听者,event被丢弃 2.你可能会需要一个事件过滤 3 工作队列: 类似广播队列,比如worker pool 1.需要调度 ``` #### 谁可以写队列 ``` 1 一个写者(类似同步observer) 1.你明确知道事件是谁发出的 2.通常允许多个读者 2 多个写者 1.注意循环 2.需要有访问发送者的途径(事件里包含sender的引用) ``` #### 队里里对象的生命周期 ``` 1. 转移所有权. 有发送者转给队列,队列转给接受者 2. 共享所有权. 3. 所有权只给队列. (队列申请内存,然后发送者填充数据, 接受者得到引用) ``` ###See also ``` 1. 队列很像是异步的observer 2. message queue, pub sub 3. 有限状态机, 状态机需要输入,如果你想异步执行,可以使用队列. 如果你需要多个状态机互相发消息, 需要一个queue接受输入(mail box), 这叫做actor model 4. go语言内置的channel本质上就是个 事件或消息 队列. ```