[续] 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游戏引擎核心之十一——并发编程(消息通知中心)的更多相关文章

  1. cocos2d-x游戏引擎核心(3.x)----事件分发机制之事件从(android,ios,desktop)系统传到cocos2dx的过程浅析

    (一) Android平台下: cocos2dx 版本3.2,先导入一个android工程,然后看下AndroidManifest.xml <application android:label= ...

  2. cocos2d-x游戏引擎核心之六——绘图原理和绘图技巧

    一.OpenGL基础 游戏引擎是对底层绘图接口的包装,Cocos2d-x 也一样,它是对不同平台下 OpenGL 的包装.OpenGL 全称为 Open Graphics Library,是一个开放的 ...

  3. Java 的核心目的和并发编程

    读一本书,最好能从它的前言开始.那么我们就来看看<Java编程思想>作者 Bruce Eckel 在前言里都说了些什么吧. 01.Java 的核心目的是"为程序员减少复杂性&qu ...

  4. cocos2d-x游戏引擎核心之八——多线程

    一.多线程原理 (1)单线程的尴尬 重新回顾下 Cocos2d-x 的并行机制.引擎内部实现了一个庞大的主循环,在每帧之间更新各个精灵的状态.执行动作.调用定时函数等,这些操作之间可以保证严格独立,互 ...

  5. cocos2d-x游戏引擎核心之七——数据持久化

    一.XML与JSON XML 和 JSON 都是当下流行的数据存储格式,它们的共同特点就是数据明文,十分易于阅读.XML 源自于 SGML,是一种标记性数据描述语言,而 JSON 则是一种轻量级数据交 ...

  6. cocos2d-x游戏引擎核心(3.x)----启动渲染流程

    (1) 首先,这里以win32平台下为例子.win32下游戏的启动都是从win32目录下main文件开始的,即是游戏的入口函数,如下: #include "main.h" #inc ...

  7. cocos2d-x游戏引擎核心之九——跨平台

    一.cocos2d-x跨平台 cocos2d-x到底是怎样实现跨平台的呢?这里以Win32和Android为例. 1. 跨平台项目目录结构 先看一下一个项目创建后的目录结构吧!这还是以HelloCpp ...

  8. cocos2d-x游戏引擎核心之四——动作调度机制

    一.动作机制的用法 在深入学习动作机制在 Cocos2d-x 里是如何实现的之前,我们先来学习整套动作机制的用法,先知道怎么用,再深入学习它如何实现,是一个很好很重要的学习方法. (1)基本概念 CC ...

  9. cocos2d-x游戏引擎核心之十二——3.x新特性

    v3.0 亮点 使用 C++(C++11) 的特性取代了 Objective-C 的特性 优化了 Labels 优化了渲染器(比 v2.2 更快) 新的事件分发机制 物理引擎集成 新的 UI 对象 J ...

随机推荐

  1. C语言 · 拿糖果

    算法提高 拿糖果   时间限制:1.0s   内存限制:256.0MB      问题描述 妈妈给小B买了N块糖!但是她不允许小B直接吃掉. 假设当前有M块糖,小B每次可以拿P块糖,其中P是M的一个不 ...

  2. 无法识别的属性 configProtectionProvider的解决方案

    用RsaProtectedConfigurationProvider加密数据库连接字符串时,只要App.config有任何改动,都会提示无法识别的属性 configProtectionProvider ...

  3. http://blog.csdn.net/beitiandijun/article/details/41678251

    http://blog.csdn.net/beitiandijun/article/details/41678251

  4. Testng 的数据源 驱动測试 代码与配置

    JUnit中有讲述使用注解的方式进行数据源读取进行自己主动循环測试的方法,在TestNG中也提供了对应的方法 public class TestngDataProvider { /** * 数组内的每 ...

  5. java-TokenProcessor令牌校验工具类

    TokenProcessor令牌校验工具类 public class TokenProcessor { private long privious;// 上次生成表单标识号得时间值 private s ...

  6. 创建ajax对象并兼容多个浏览器方法简单记录

    这篇文章主要介绍了如何创建ajax对象并兼容多个浏览器,需要的朋友可以参考下<script> function createAjax(){ var request=false; //win ...

  7. 7 款基于 JavaScript/AJAX 的文件上传插件

    本文整理了7款基于JavaScript和AJAX的文件上传插件,这些插件基本上都能实现以下功能: 多文件上传 拖拽操作 实时上传进度 自定义上传限制 希望能为你的开发工作带来帮助. 1.  jQuer ...

  8. 7 款灵巧实用的 CSS3/jQuery 工具

    作为 Web 前端开发者,应该对 jQuery 比较熟悉,对免费开源的 jQuery 也用的非常多.但是随着 CSS3 标准的诞生和发展,很多 jQuery 插件也都纷纷应用了 CSS3 新标准,也因 ...

  9. 【转】【VC】VC程序运行时间测试函数

    1:Sleep函数 使用: sleep(1000),在Windows和Linux下1000代表的含义并不相同,Windows下的表示1000毫秒,也就是1秒钟: Linux下表示1000秒,Linux ...

  10. PHP利用memcache缓存技术提高响应速度

    PHP下memcache模块是一个高效的守护进程,提供用于内存缓存的过程式程序和面向对象的方便的接口,特别是对于设计动态web程序时减少对数据库的访问.memcache也提供用于通信对话(sessio ...