[续] 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. 传智播客《巴巴运动网视频教程(11-106)》avi格式以及兴许44集视频包括所有源码和资源

    (1)网上找巴巴运动网代码资源的时候找了非常久 基本上都是须要各种积分的 最终找到了一个不须要积分的推荐给大家.(支持迅雷下载) (2)兴许44集的jar包和项目文档等下载地址! watermark/ ...

  2. 纯css实现进度条效果

    去年7月份做一个公司商城的微信页面(微信用的chrome内核)需要写一个提示返现进度的进度条效果. 一个完整的进度条效果其实可以拆分一下: 一段背景: 一小段的静态的斜纹进度条: 斜纹进度条用线性渐变 ...

  3. C语言 · 用宏求球的体积

    算法提高 7-1用宏求球的体积   时间限制:1.0s   内存限制:256.0MB      问题描述 使用宏实现计算球体体积的功能.用户输入半径,系统输出体积.不能使用函数,pi=3.141592 ...

  4. Java中 堆 栈,常量池等概念解析(转载)

    1.寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制. 2. 栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(字符 ...

  5. JavaScrip——练习(做悬浮框)

    通过HTML.CSS.JSP来实现 1.首先确定通过div嵌套来实现: 大的div里放默认显示的一层,限制其总层次高,设置超出部分隐藏 小的div里放鼠标移过去时显示的一层:3行1列的表格 1.1.什 ...

  6. Nutch系列1:简介

    由Java实现的,开放源代码(open-source)的web搜索引擎. Nutch 致力于让每个人都能很容易, 同时花费很少就可以配置世界一流的Web搜索引擎. 为了完成这一宏伟的目标, Nutch ...

  7. input输入框用el对数字格式化

    <input name="doubleInput" type="text" maxlength="32" id="doubl ...

  8. 安装 Windows SDK for Windows 7 时遇到的一个问题及解决办法

    最近试着用 VS2010 + Qt 开发程序,发现 VS2010 里面没有提供单独的调试器 cdb,这样用 Qt Creator 时就无法设置断点调试,很不方便.想起 Windows SDK for  ...

  9. Jquery与.net MVC结合,通过Ajax

    在工作中做了这么一个东西. Html端: @using Test.fh.Project.Storefront.ViewModels @using Test.fh.Project.Storefront. ...

  10. 【转】【Java/Android】Toast使用方法大全

    Toast 是一个 View 视图,快速的为用户显示少量的信息. Toast 在应用程序上浮动显示信息给用户,它永远不会获得焦点,不影响用户的输入等操作,主要用于 一些帮助 / 提示.Toast 最常 ...