cocos2d-x游戏引擎核心之十一——并发编程(消息通知中心)
这里介绍cocos2d-x的一种消息/数据传递方式,内置的观察者模式,也称消息通知中心,CCNotificationCenter。

虽然引擎没有为我们封装线程类,但还是提供了一些组件,辅助我们进行并发编程。除了上面提到的异步加载图片,引擎还提供了消息中心 CCNotificationCenter。这是一个类似 Qt 中消息槽的机制,一个对象可以注册到消息中心,指定要接收的消息;而某个事件完成时,则可以发送对应的消息,之前注册过的对象会得到通知。主要有以下两个关键的接口函数:
void addObserver(CCObject *target, //接收消息的对象
SEL_CallFuncO selector, //响应消息的函数
const char *name, //待接收的消息
CCObject *obj); //指定消息的发送者,目前暂时为无用参数
void postNotification(const char *name); //发送一个消息
借助消息中心,异步事件之间的对象可以进一步减少耦合,使用事件驱动的方式编写代码。以游戏中的金币数变动为例,我们将菜单层添加为金币数量变化的消息的观察者,相关代码如下:
CCNotificationCenter::sharedNotificationCenter()->addObserver(this,callfuncO_selector(GameMenuLayer::coinChange), "CoinChange", NULL);
然后在开炮、捕获鱼等引起金币变化的地方发出该消息,从而触发菜单层的 coinChange 函数:
CCNotificationCenter::sharedNotificationCenter()->postNotification("CoinChange", NULL);
当然,在多线程的环境中,考虑到之前提到的原则,不可能直接在分离的线程中调用消息中心发送消息,我们可以建立一个线程间共享的消息池,让消息可以在不同线程间流动,或者说,我们需要建立一个线程安全的消息队列。下面我们创建一个线程安全的消息队列,代码如下:
class MTNotificationQueue : CCNode
{
typedef struct
{
string name;
CCObject* object;
} NotificationArgs;
vector<NotificationArgs> notifications;
MTNotificationQueue(void);
public:
static MTNotificationQueue* sharedNotificationQueue();
void postNotifications(ccTime dt);
~MTNotificationQueue(void);
void postNotification(const char* name, CCObject* object);
};
从接口上看,这个消息队列可以看做引擎自带的消息中心的补充,因为这里并不提供消息接收者的注册,仅仅是允许线程安全地向消息中心发出一个消息。这样也对应了一种处理模式:主线程负责绘图实现,在分离出来的子线程中完成重计算任务,计算完成后向主线程发回处理完毕的消息,消息是单向流动的,数据从磁盘、网络或其他任何地方经过处理后最终以视图的形式流向了屏幕。
在实现上,我们通过一个数组缓冲了各线程间提交的消息,稍后在主线程中将这些消息一次性地向 CCNotificationCenter发出。其中需要保证的是,缓冲用的数组在不同线程间的访问必须是安全的,因此需要一个互斥锁。
不同线程间可共享的数据必须是静态的或全局的,因此互斥锁也必须是全局的。考虑到这个消息队列应该是全局唯一的单例,仅仅需要一个全局唯一的互斥锁与之对应:
pthread_mutex_t sharedNotificationQueueLock;
而考虑到这个互斥锁必须进行合适的初始化和清理,可以用一个类的全局变量管理其生命周期:
class LifeManager_PThreadMutex
{
pthread_mutex_t* mutex;
public:
LifeManager_PThreadMutex(pthread_mutex_t* mut) : mutex(mut)
{
pthread_mutex_init(mutex, NULL);
}
~LifeManager_PThreadMutex()
{
pthread_mutex_destroy(mutex);
}
}__LifeManager_sharedNotificationQueueLock(&sharedNotificationQueueLock);
在 pthread 库中,我们使用下面一对函数进行互斥锁的上锁和解锁:
int pthread_mutex_lock (pthread_mutex_t * mutex); //上锁
int pthread_mutex_unlock (pthread_mutex_t * mutex); //解锁
这里的上锁函数是阻塞性的,如果目标互斥锁已经被锁上,会一直阻塞线程直到解锁,然后再次尝试解锁直到成功从当前线程上锁为止。
同样,考虑到上锁过程往往对应了一段函数或一个程序段的开始和结束,可以对应到一个临时变量的生命周期中,我们再次封装一个"生命周期锁类":
class LifeCircleMutexLocker
{
pthread_mutex_t* mutex;
public:
LifeCircleMutexLocker(pthread_mutex_t* aMutex) : mutex(aMutex)
{
pthread_mutex_lock(mutex);
}
~LifeCircleMutexLocker(){
pthread_mutex_unlock(mutex);
}
};
#define LifeCircleMutexLock(mutex) LifeCircleMutexLocker __locker__(mutex)
一切准备就绪后,就剩下两个核心的接口函数--向队列发出消息以及由队列将消息发到消息中心中,相关代码如下:
//由队列将消息发到消息中心
void MTNotificationQueue::postNotifications(ccTime dt)
{
//生命周期锁
// 用一个类LifeCircleMutexLock管理互斥锁sharedNotificationQueueLock的生命周期
LifeCircleMutexLock(&sharedNotificationQueueLock);
for(int i = ; i < notifications.size(); i++) {
NotificationArgs &arg = notifications[i];
// 调用主线程通知函数,将所有消息发送到消息中心
CCNotificationCenter::sharedNotificationCenter()->postNotification(arg.name.c_str(), arg.object);
}
notifications.clear();
} // 向队列发出消息
void MTNotificationQueue::postNotification(const char* name, CCObject* object)
{
//生命周期锁
LifeCircleMutexLock(&sharedNotificationQueueLock);
NotificationArgs arg;
arg.name = name;
if(object != NULL)
arg.object = object->copy();
else
arg.object = NULL;
notifications.push_back(arg);
}
实际上,这是两个非常简短的函数,仅仅是将传入的消息缓冲到数组中并取出。唯一的特别之处只在于函数在开始时,使用了我们前面定义的"生命周期锁",保证了在访问缓冲数组的过程中是线程安全的,整个读写过程中缓冲数组由当前线程独占。
最后,我们启动消息队列的定时器,使 postNotifications 函数每帧被调用,保证不同线程间发出的消息能第一时间送达主线程:
CCDirector::sharedDirector()->getScheduler()->scheduleSelector(
schedule_selector(MTNotificationQueue::postNotifications),
MTNotificationQueue::sharedNotificationQueue(),
1.0 / 60.0,
false);
有了这个消息池,就可以进一步简化之前的图片加载过程了。下面仍然使用背景层的例子,再次重写游戏背景层的初始化函数:
bool BackgroundLayer::init()
{
LOG_FUNCTION_LIFE;
bool bRet = false;
do {
CC_BREAK_IF(! CCLayer::init());
CCNotificationCenter::sharedNotificationCenter()->addObserver(
this,
callfuncO_selector(BackgroundLayer::loadImageFinish),
"loadImageFinish",
NULL);
pthread_t tid;
pthread_create(&tid, NULL, &loadImages, NULL);
bRet = true;
} while ();
return bRet;
}
我们不再按照注释中的做法那样使用系统的纹理缓存来异步添加背景图片,而是先注册到消息中心,而后主动创建一个线程负责加载图片。在该线程中,我们仅完成图片向内存的加载,相关代码如下:
void* loadImages(void* arg)
{
bgImage = new CCImage();
bgImage->initWithImageFileThreadSafe("background.png");
MTNotificationQueue::sharedNotificationQueue()->postNotification("loadImageFinish", NULL);
return NULL;
}
在加载完成之后,我们通过消息队列发出了一个加载完成的消息,在稍后的消息队列更新时,这个消息将会被发送到消息中心,而后通知到背景层的响应函数中。我们为背景层添加相应的响应函数 loadImageFinish,其代码如下:
void BackgroundLayer::loadImageFinish(CCObject* sender)
{
CCSize winSize = CCDirector::sharedDirector()->getWinSize();
CCTexture2D* texture = CCTextureCache::sharedTextureCache()->addUIImage(bgImage, "background.png");
bgImage->release();
CCSprite* bg = CCSprite::create(texture);
CCSize size = bg->getContentSize();
bg->setPosition(ccp(winSize.width / , winSize.height / ));
float f = max(winSize.width/size.width,winSize.height/size.height);
bg->setScale(f);
this->addChild(bg);
}
这里 bgImage 是用 new 方式创建的,堆空间是除了全局静态对象之外唯一可以在线程间共享的数据空间。
必须注意的是,作为共享数据的 bgImage 的内存管理方式,我们在加载线程中用 new 从堆空间中分配了该内存,但是并没有遵守内存管理规范在该函数中将其释放,因为此时 bgImage 还未使用完毕,也不可能调用自动释放池,因为在子线程中是不存在自动释放池的,如果跨线程调用了自动释放池,将造成严重的紊乱。因此,我们最后在 loadimageFinish 中添加到纹理缓存后才将其释放。
这也是使用多线程进行并发编程时的一个比较大的障碍,由于引擎的内存管理体系 CCObject 是非线程安全的,而整个引擎又是搭建在 CCObject 提供的内存管理机制基础上的,因此我们在多线程环境中使用引擎的任何对象都必须分外小心。
二、cocos2dx消息中心:
自行查阅相关源码, 没有使用多线程.
使用CCNotificationCenter需要注意以下几点:
(1)一个对象可以注册多个消息,一个消息也可以由多个消息注册。
(2)传递参数,A可以向B传递参数,而B在注册的时候也可以带一个参数,如果这两个数据不是指向同一对象的话,消息不会传递。也就是说要么A传递NULL对象,要么B注册时带NULL对象,要么都不是NULL但必须是同一对象,消息传递才会成功。以下是发送消息执行的判断:
if (!strcmp(name,observer->getName()) && (observer->getObject() == object || observer->getObject() == NULL || object == NULL))
(3)局部变量的传递,注意到上例,传递的是CCString的一个局部变量(但还是要autorelease),从CCNotificationCenter的实现上来看,这是没有问题的,因为数据是在postNotification被调用的,也就是整个函数体并没结束,数据不会被销毁。
cocos2d-x游戏引擎核心之十一——并发编程(消息通知中心)的更多相关文章
- cocos2d-x游戏引擎核心(3.x)----事件分发机制之事件从(android,ios,desktop)系统传到cocos2dx的过程浅析
(一) Android平台下: cocos2dx 版本3.2,先导入一个android工程,然后看下AndroidManifest.xml <application android:label= ...
- cocos2d-x游戏引擎核心之六——绘图原理和绘图技巧
一.OpenGL基础 游戏引擎是对底层绘图接口的包装,Cocos2d-x 也一样,它是对不同平台下 OpenGL 的包装.OpenGL 全称为 Open Graphics Library,是一个开放的 ...
- Java 的核心目的和并发编程
读一本书,最好能从它的前言开始.那么我们就来看看<Java编程思想>作者 Bruce Eckel 在前言里都说了些什么吧. 01.Java 的核心目的是"为程序员减少复杂性&qu ...
- cocos2d-x游戏引擎核心之八——多线程
一.多线程原理 (1)单线程的尴尬 重新回顾下 Cocos2d-x 的并行机制.引擎内部实现了一个庞大的主循环,在每帧之间更新各个精灵的状态.执行动作.调用定时函数等,这些操作之间可以保证严格独立,互 ...
- cocos2d-x游戏引擎核心之七——数据持久化
一.XML与JSON XML 和 JSON 都是当下流行的数据存储格式,它们的共同特点就是数据明文,十分易于阅读.XML 源自于 SGML,是一种标记性数据描述语言,而 JSON 则是一种轻量级数据交 ...
- cocos2d-x游戏引擎核心(3.x)----启动渲染流程
(1) 首先,这里以win32平台下为例子.win32下游戏的启动都是从win32目录下main文件开始的,即是游戏的入口函数,如下: #include "main.h" #inc ...
- cocos2d-x游戏引擎核心之九——跨平台
一.cocos2d-x跨平台 cocos2d-x到底是怎样实现跨平台的呢?这里以Win32和Android为例. 1. 跨平台项目目录结构 先看一下一个项目创建后的目录结构吧!这还是以HelloCpp ...
- cocos2d-x游戏引擎核心之四——动作调度机制
一.动作机制的用法 在深入学习动作机制在 Cocos2d-x 里是如何实现的之前,我们先来学习整套动作机制的用法,先知道怎么用,再深入学习它如何实现,是一个很好很重要的学习方法. (1)基本概念 CC ...
- cocos2d-x游戏引擎核心之十二——3.x新特性
v3.0 亮点 使用 C++(C++11) 的特性取代了 Objective-C 的特性 优化了 Labels 优化了渲染器(比 v2.2 更快) 新的事件分发机制 物理引擎集成 新的 UI 对象 J ...
随机推荐
- Excel VBA 操作 Word(入门篇)
原文地址 本文的对象是:有一定Excel VBA基础,对Word VBA还没有什么认识,想在Excel中通过VBA操作Word还有困难的人. 一.新建Word引用 需要首先创建一个对 Word A ...
- ExtJs GridPanel 给表格行或者单元格自定义样式
Ext.onReady(function(){ Ext.create('Ext.data.Store', { storeId:'simpsonsStore', fields:['name', 'ema ...
- Andriod——数据存储 SharedPrefrences
xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android= ...
- socket数据收发
socket读写 TCP协议是面向流的,read和write调用的返回值往往小于参数指定的字节数.对于read调用,如果接收缓冲区中有20字节,请求读100个字节,就会返回20.对于write调用,如 ...
- 机器学习:K-Means聚类算法
本文来自同步博客. 前面几篇文章介绍了回归或分类的几个算法,它们的共同点是训练数据包含了输出结果,要求算法能够通过训练数据掌握规律,用于预测新输入数据的输出值.因此,回归算法或分类算法被称之为监督学习 ...
- scala实现彩票算法
scala实现彩票算法 (1)具体实现代码如下: package hw1 import scala.util.control._ /** * @author BIGDATA */ object Cp ...
- Phalcon Framework的Mvc结构及启动流程(部分源码分析)
创建项目 Phalcon环境配置安装后,可以通过命令行生成一个标准的Phalcon多模块应用 phalcon project eva --type modules入口文件为public/index.p ...
- MySQL5.7远程连接和增加密码
主要是5.7的很多操作和以前版本不一样,所以踩了很多坑. 1. 远程连接cant connect to mysql (10061) 一开始以为是权限问题,所以参考了详解 MySQL 5.7 新的权限与 ...
- 为什么手机无法执行应用? Values之谜
欢迎Follow我的GitHub, 关注我的CSDN, 精彩不断! CSDN: http://blog.csdn.net/caroline_wendy/article/details/68923156 ...
- JEECG常见问题大全征集
大家还有什么问题.请跟帖,谢谢支持. . JEECG常见问题大全征集 1. jeecg没有数据库脚本问题 jeecg不须要数据库脚本,在数据库创建好数据库.项目配置好数据源链接.会自己主动建表. ...