cocos2dx与多线程

cocos2dx引擎是一个单线程的引擎。

pthread 是一套 POSIX 标准线程库,可以运行在各个平台上,包括 Android、iOS 和 Windows,也是 Cocos2d-x 官方推荐的多线程库。

创建一个线程的方法叫pthread_create,它的函数原型是PTW32_DLLPORTint PTW32_CDECL pthread_create (pthread_t * tid,const pthread_attr_t * attr,void *(*start) (void *),void *arg);它的第一个参数指定一个标识的地址,用于返回创建的线程标识;第二个参数是创建线程的参数,在不需要设置任何参数的情况下,只需传入 NULL 即可;第三个参数则是线程入口函数的指针,被指定为 void*(void*)的形式。函数指针接受的唯一参数来源于调用 pthread_create 函数时所传入的第四个参数,可以用于传递用户数据。

下面写一个创建线程的例子:

void* justAnotherTest(void *arg)
{
    LOG_FUNCTION_LIFE;
    //在这里写入新线程将要执行的代码
    return NULL;
}
void testThread()
{
    LOG_FUNCTION_LIFE;
    pthread_t tid;
    pthread_create(&tid, NULL, &justAnotherTest, NULL);
}

用pthread_create创建了一个线程,新线程的入口为justAnotherTest函数。

使用线程就不得不提线程安全问题。线程安全问题来源于不同线程的执行顺序是不可预测的,线程调度都视系统当时的状态而定,尤其是直接或间接的全局共享变量。如果不同线程间都存在着读写访问,就很可能出现运行结果不可控的问题。

在 Cocos2d-x 中,最大的线程安全隐患是内存管理。引擎明确声明了 retain、release 和 autorelease 三个方法都不是线程安全的。如果在不同的线程间对同一个对象作内存管理,可能会出现严重的内存泄露或野指针问题。比如说,如果我们按照下述代码加载图片资源,就很可能出现找不到图片的报错——可能出现这样的情况,当主线程执行到CCSprite::Create创建精灵的时候,上面的线程还没有执行或者没有执行完成图片资源的加载,这时就可能出现找不到图片。

void* loadResources(void *arg)
{
    LOG_FUNCTION_LIFE;
    CCTextureCache::sharedTextureCache()->addImage("fish.png");
    return NULL;
}
void makeAFish()
{
    LOG_FUNCTION_LIFE;
    pthread_t tid;
    pthread_create(&tid, NULL, &loadResources, NULL);
    CCSprite* sp = CCSprite::create("fish.png");
}

在新的线程中对缓存的调用所产生的一系列内存管理操作更可能导致系统崩溃。

  因此,使用多线程的首要原则是,在新建立的线程中不要使用任何 Cocos2d-x 内建的内存管理,也不要调用任何引擎提供的函数或方法,因为那可能会导致 Cocos2d-x 内存管理错误。

  同样,OpenGL 的各个接口函数也不是线程安全的。也就是说,一切和绘图直接相关的操作都应该放在主线程内执行,而不是在新建线程内执行。

 

使用并发编程的最直接目的是保证界面流畅,这也是引擎占据主线程的原因。因此,除了界面相关的代码外,其他操作都可以放入新的线程中执行,主要包括文件读写和网络通信两类。

  文件读写涉及外部存储操作,这和内存、CPU 都不在一个响应级别上。如果将其放入主线程中,就可能会造成阻塞,尤为严重的是大型图片的载入。对于碎图压缩后的大型纹理和高分辨率的背景图,一次加载可能耗费 0.2 s 以上的时间,如果完全放在主线程内,会阻塞主线程相当长的时间,导致画面停滞,游戏体验很糟糕。在一些大型的卷轴类游戏中,这类问题尤为明显。考虑到这个问题,Cocos2d-x 为我们提供了一个异步加载图片的接口,不会阻塞主线程,其内部正是采用了新建线程的办法。

我们用游戏中的背景层为例,原来加载背景层的操作是串行的,现在我们将这一些列串行的过程分离开来,使用引擎提供的异步加载图片接口异步加载图片,相关代码如下:

void BackgroundLayer::doLoadImage(ccTime dt)
{
    CCSize winSize = CCDirector::sharedDirector()->getWinSize();
    CCSprite *bg = CCSprite::create("background.png");
    CCSize size = bg->getContentSize();
    bg->setPosition(ccp(winSize.width / 2, winSize.height /2));
    float f = max(winSize.width/size.width,winSize.height/size.height);
    bg->setScale(f);
    this->addChild(bg);
}

void BackgroundLayer::loadImageFinish(CCObject* sender)
{
    this->scheduleOnce(schedule_selector(BackgroundLayer::doLoadImage),2);
}

bool BackgroundLayer::init()
{
    LOG_FUNCTION_LIFE;
    bool bRet = false;
    do {
        CC_BREAK_IF(! CCLayer::init());
        CCTextureCache::sharedTextureCache()->addImageAsync(
        "background.png",
        this,
        callfuncO_selector(BackgroundLayer::loadImageFinish));
        bRet = true;
    } while (0);
    return bRet;
}

为了加强效果的对比,我们在图片加载成功后,延时了 2 s,而后才真正加载背景图片到背景层中。读者可以明显看到,2s后游戏中才出现了背景图。尽管引擎已经为我们提供了异步加载图片缓存的方式,但考虑到对图片资源的加密解密过程是十分耗费计算资源的,我们还是有必要单开一个线程执行这一系列操作。另一个值得使用并发编程的是网络通信。网络通信可能比文件读写要慢一个数量级。一般的网络通信库都会提供异步传输形式,我们只需要注意选择就好。

使用了线程,必然就要考虑到线程同步,不同的线程同时访问资源的话,访问的顺序是不可预知的,会造成不可预知的结果。查看addImageAsync的实现源码可以知道它是使用pthread_mutex_t来实现同步。

当我们想在程序中开多线程中,第一想到的是cocos2d-x有没有自带方法,幸运的是我们找到了CCThread,不幸却发现里面什么都没有。cocos2d-x自带了一个第三方插件--pthread,在cocos2dx\platform\third_party\win32\pthread可以找到。既然是自带的,必须它的理由。想在VS中应用这个插件需要两个步骤:

1.需要右键工程--属性--配置属性--链接器--输入--编缉右侧的附加依赖项--在其中添加pthreadVCE2.lib

2..需要右键工程--属性--配置属性--C/C++--常规--编缉右侧的附加包含目录--添加新行--找到pthread文件夹所在位置

然后我们就可以应用这个插件在程序中开启新线程,简单线程开启方法如下代码所示:

#ifndef _LOADING_SCENE_H__ 
#define _LOADING_SCENE_H__  
  
#include "cocos2d.h" 
#include "pthread/pthread.h" 
class LoadingScene : public cocos2d::CCScene{ 
public:  
    virtual bool init(); 
    CREATE_FUNC(LoadingScene);  
    int start();    
    void update(float dt); 
private:  
    pthread_t pid;  
    static void* updateInfo(void* args);//注意线程函数必须是静态的  
};

#include"LoadingScene.h" 
#include "pthread/pthread.h" 
  
using namespace cocos2d; 
bool LoadingScene::init(){  
    this->scheduleUpdate(); 
    start();  
    return true; 
}  
void LoadingScene::update(float dt){ 
           //可以在这里重绘UI  
}  
void* LoadingScene::updateInfo(void* args){ 
      //可以在这里加载资源  
    return NULL;  
}  
int LoadingScene::start(){  
    pthread_create(&pid,NULL,updateInfo,NULL);//开启新线程  
    return 0; 
}

我们可以在新开的线程中,加载资源,设置一个静态变量bool,在新线程中,当加载完所有资源后,设置bool值为真。在主线程中Update中,检测bool值,为假,可以重绘UI(例如,显示加载图片,或者模拟加载进度),为真,则加载目标场景。相关代码如下:

void* LoadingScene::updateInfo(void* args){  
     CCSpriteFrameCache *cache = CCSpriteFrameCache::sharedSpriteFrameCache(); 
     cache->addSpriteFramesWithFile("BattleIcons.plist"); 
     cache->addSpriteFramesWithFile("ArcherAnim.plist"); 
     cache->addSpriteFramesWithFile("DeathReaperAnim.plist"); 
     loadComplete = true; //状态值设为真,表示加载完成  
     return NULL;  

成功加载且运行后,你会发现新场景中所有精灵都不显示(类似于黑屏了)。为什么呢?

  因为我们在加载plist文件时,addSpriteFramesWithFile方法里会帮我们创建plist对应Png图的Texture2D,并将其加载进缓存中。可是这里就遇到了一个OpenGl规范的问题:不能在新开的线程中,创建texture,texture必须在主线程创建.通俗点,就是所有的opengl api都必须在主线程中调用;其它的操作,比如文件,内存,plist等,可以在新线程中做,这个不是cocos2d不支持,是opengl的标准,不管你是在android,还是windows上使用opengl,都是这个原理。

  所以不能在新线程中创建Texture2D,导致纹理都不显示,那么该怎么办?让我们看看CCSpriteFrameCache源码,发现CCSpriteFrameCache::addSpriteFramesWithFile(const char *pszPlist, CCTexture2D *pobTexture)方法,是可以传入Texture2D参数的。是的,我们找到了解决方法:

int LoadingScene::start(){  
    CCTexture2D *texture = CCTextureCache::sharedTextureCache()->addImage("BattleIcons.png");//在这里(主线程中)加载plist对应的Png图片进纹理缓存  
    CCTexture2D *texture2 = CCTextureCache::sharedTextureCache()->addImage("ArcherAnim.png");//以这种方法加载的纹理,其Key值就是文件path值,即例如  
texture2的key值就是ArcherAnim.png
    CCTexture2D *texture3 = CCTextureCache::sharedTextureCache()->addImage("DeathReaperAnim.png"); 
    pthread_create(&pid,NULL,updateInfo,NULL); //开启新线程 
    return 0; 
}  
void* LoadingScene::updateInfo(void* args){ 
    CCSpriteFrameCache *cache = CCSpriteFrameCache::sharedSpriteFrameCache(); 
    CCTextureCache* teCache = CCTextureCache::sharedTextureCache();    
    CCTexture2D* texture1 = teCache->textureForKey("BattleIcons.png");//从纹理缓存中取出Texure2D,并将其当参数传入addSpriteFramesWithFile方法中 
    cache->addSpriteFramesWithFile("BattleIcons.plist",texture1); 
    CCTexture2D* texture2 = teCache->textureForKey("ArcherAnim.png"); 
    cache->addSpriteFramesWithFile("ArcherAnim.plist",texture2); 
    CCTexture2D* texture3 = teCache->textureForKey("DeathReaperAnim.png"); 
    cache->addSpriteFramesWithFile("DeathReaperAnim.plist",texture3); 
    loadComplete = true; 
    return NULL;  
}

这样解决,就不违背OpenGl规范,没有在新线程中创建Texture2D。

Tip:OpenGL与线程相结合时,此时你需要把你需要渲染的精灵先加载到内存中去,可以设置成为不显示,然后在线程执行后再设置精灵成显示状态,这样可以解决线程与OpneGL渲染不兼容的问题

我们经常看到一些手机游戏,启动之后首先会显示一个带有公司Logo的闪屏画面(Flash Screen),然后才会进入一个游戏Welcome场景,点击“开始”才正式进入游戏主场景。而这里Flash Screen的展示环节往往在后台还会做另外一件事,那就是加载游戏的图片资源,音乐音效资源以及配置数据读取,这算是一个“障眼法”吧,目的就是提高用 户体验,这样后续场景渲染以及场景切换直接使用已经cache到内存中的数据即可,无需再行加载。

在FlashScene init时,我们创建一个Resource Load Thread,我们用一个ResourceLoadIndicator作为渲染线程与Worker线程之间交互的媒介。

//FlashScene.h
  
struct ResourceLoadIndicator { 
    pthread_mutex_t mutex;
    bool load_done; 
    void *context;
}; 
  
class FlashScene : public Scene

public: 
    FlashScene(void); 
    ~FlashScene(void);
  
    virtual bool init();
  
    CREATE_FUNC(FlashScene); 
    bool getResourceLoadIndicator(); 
    void setResourceLoadIndicator(bool flag);
  
private: 
     void updateScene(float dt);
  
private: 
     ResourceLoadIndicator rli; 
}; 
  
// FlashScene.cpp 
bool FlashScene::init() 

    bool bRet = false;
    do { 
        CC_BREAK_IF(!CCScene::init());
        Size winSize = Director::getInstance()->getWinSize();
  
        //FlashScene自己的资源只能同步加载了 
        Sprite *bg = Sprite::create("FlashSceenBg.png");
        CC_BREAK_IF(!bg); 
        bg->setPosition(ccp(winSize.width/2, winSize.height/2));
        this->addChild(bg, 0);
  
        this->schedule(schedule_selector(FlashScene::updateScene)
                       , 0.01f); 
  
        //start the resource loading thread
        rli.load_done = false; 
        rli.context = (void*)this;
        pthread_mutex_init(&rli.mutex, NULL);
        pthread_attr_t attr; 
        pthread_attr_init(&attr); 
        pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
        pthread_t thread; 
        pthread_create(&thread, &attr,
                    resource_load_thread_entry, &rli);
  
        bRet=true;
    } while(0);
  
    return bRet; 

  
static void* resource_load_thread_entry(void* param)

    AppDelegate *app = (AppDelegate*)Application::getInstance();
    ResourceLoadIndicator *rli = (ResourceLoadIndicator*)param;
    FlashScene *scene = (FlashScene*)rli->context;
  
    //load music effect resource 
    … … 
  
    //init from config files 
    … … 
  
    //load images data in worker thread
    SpriteFrameCache::getInstance()->addSpriteFramesWithFile(// 函数内部会进行纹理创建,不能再非主线程中调用cocos2dx内部函数或egl图形api
                                       "All-Sprites.plist");
    … … 
  
    //set loading done 
    scene->setResourceLoadIndicator(true);
    return NULL; 

  
bool FlashScene::getResourceLoadIndicator()

    bool flag; 
    pthread_mutex_lock(&rli.mutex); 
    flag = rli.load_done; 
    pthread_mutex_unlock(&rli.mutex);
    return flag; 

  
void FlashScene::setResourceLoadIndicator(bool flag)

    pthread_mutex_lock(&rli.mutex); 
    rli.load_done = flag; 
    pthread_mutex_unlock(&rli.mutex);
    return; 
}

我们在定时器回调函数中对indicator标志位进行检查,当发现加载ok后,切换到接下来的游戏开始场景: 

void FlashScene::updateScene(float dt) 

    if (getResourceLoadIndicator()) {
        Director::getInstance()->replaceScene(
                              WelcomeScene::create()); 
    } 
}

到此,FlashScene的初始设计和实现完成了。Run一下试试吧。

在GenyMotion的4.4.2模拟器上,游戏运行的结果并没有如我期望,FlashScreen显现后游戏就异常崩溃退出了。通过monitor分析游戏的运行日志,我们看到了如下一些异常日志: 

threadid=24: thread exiting, not yet detached (count=0)
threadid=24: thread exiting, not yet detached (count=1)
threadid=24: native thread exited without detaching

很是奇怪啊,我们在创建线程时,明明设置了 PTHREAD_CREATE_DETACHED属性了啊:

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

怎么还会出现这个问题,而且居然有三条日志。翻看了一下引擎内核的代码TextureCache::addImageAsync,在线程创建以及线程主函数中也没有发现什么特别的设置。为何内核可以创建线程,我自己创建就会崩溃呢。Debug多个来回,问题似乎聚焦在resource_load_thread_entry中执行的任务。在我的代码里,我利用SimpleAudioEngine加载了音效资源、利用UserDefault读取了一些持久化的数据,把这两个任务去掉,游戏就会进入到下一个环节而不会崩溃。

SimpleAudioEngine和UserDefault能有什么共同点呢?Jni调用。没错,这两个接口底层要适配多个平台,而对于Android 平台,他们都用到了Jni提供的接口去调用Java中的方法。而Jni对多线程是有约束的。Android开发者官网上有这么一段话:

All threads are Linux threads, scheduled by the kernel. They're usually started from managed code (using Thread.start), but they can also be created elsewhere and then attached to the JavaVM. For example, a thread started with pthread_create can be attached with the JNI AttachCurrentThread or AttachCurrentThreadAsDaemon functions. Until a thread is attached, it has no JNIEnv, and cannot make JNI calls.

由此看来pthread_create创建的新线程默认情况下是不能进行Jni接口调用的,除非Attach到Vm,获得一个JniEnv对象,并且在线程exit前要Detach Vm。好,我们来尝试一下,Cocos2d-x引擎提供了一些JniHelper方法,可以方便进行Jni相关操作。

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include "platform/android/jni/JniHelper.h"
#include <jni.h> 
#endif 
  
static void* resource_load_thread_entry(void* param)

    … … 
  
    JavaVM *vm; 
    JNIEnv *env; 
    vm = JniHelper::getJavaVM(); 
  
    JavaVMAttachArgs thread_args; 
  
    thread_args.name = "Resource Load";
    thread_args.version = JNI_VERSION_1_4;
    thread_args.group = NULL; 
  
    vm->AttachCurrentThread(&env, &thread_args);
    … … 
    //Your Jni Calls 
    … … 
  
    vm->DetachCurrentThread(); 
    … … 
    return NULL; 
}

关于什么是JavaVM,什么是JniEnv,Android Developer官方文档中是这样描述的

  The JavaVM provides the "invocation interface" functions, which allow you to create and destroy a JavaVM. In theory you can have multiple JavaVMs per process, but Android only allows one.

  The JNIEnv provides most of the JNI functions. Your native functions all receive a JNIEnv as the first argument.

  The JNIEnv is used for thread-local storage. For this reason, you cannot share a JNIEnv between threads.

上面的代码成功解决了线程崩溃的问题,但问题还没完,因为接下来我们又遇到了“黑屏”事件。所谓的“黑屏”,其实并不是全黑。但进入游戏 WelcomScene时,只有Scene中的LabelTTF实例能显示出来,其余Sprite都无法显示。显然肯定与我们在Worker线程加载纹理资源有关了: 

libEGL: call to OpenGL ES API with no current context (logged once per thread)

 

通过Google得知,只有Renderer Thread才能进行egl调用,因为egl的context是在Renderer Thread创建的,Worker Thread并没有EGL的context,在进行egl操作时,无法找到context,因此操作都是失败的,纹理也就无法显示出来。要解决这个问题就 得查看一下TextureCache::addImageAsync是如何做的了。

  TextureCache::addImageAsync只是在worker线程进行了image数据的加载,而纹理对象Texture2D instance则是在addImageAsyncCallBack中创建的。也就是说纹理还是在Renderer线程中创建的,因此不会出现我们上面的 “黑屏”问题。模仿addImageAsync,我们来修改一下代码:

staticvoid* resource_load_thread_entry(void* param)

    … … 
    allSpritesImage = new Image();
    allSpritesImage->initWithImageFile("All-Sprites.png");
    … … 

  
void FlashScene::updateScene(float dt)

    if (getResourceLoadIndicator()) {
        // construct texture with preloaded images
        Texture2D *allSpritesTexture = TextureCache::getInstance()->
                           addImage(allSpritesImage, "All-Sprites.png");
        allSpritesImage->release(); 
        SpriteFrameCache::getInstance()->addSpriteFramesWithFile(
                           "All-Sprites.plist",allSpritesTexture); 
      
        Director::getInstance()->replaceScene(WelcomeScene::create());
    } 
}

完成这一修改后,游戏画面就变得一切正常了,多线程资源加载机制正式生效。

 

 

 

游戏需要使用到多线程技术,导致测试的时候总是莫名其妙的导致空指针错误。而且是随机出现,纠结了2天无果后,开始怀疑Cocos2d-X的内 存本身管理可能存在问题。怀着这样的想法,一步一步的调试,发现经常出现指针异常的变量总是在调用autorelease一会后,再使用的时候就莫名其妙 抛异常。狠下心,在它的析构函数里面断点+Log输出信息。发现对象被释放了。一时也很迷糊,因为对象只是autorelease,并没有真正释放,是谁 导致它释放的?

然后就去看了CCAutoreleasePool的源码,发现Cocos2d-X的内存管理在多线程的情况下存在如下问题:

如图:thread 1和thread 2是独立的两个线程,它们之间存在CPU分配的交叉集,我们在time 1的时候push一个autorelease的自动释放池,在该线程的末尾,即time 3的时候pop它。同理在thread 2的线程里面,在time 2的时候push一个自动释放池,在time 4的时候释放它,即Pop.

  此时我们假设在thread 2分配得到CPU的时候有一个对象obj自动释放(在多线程下,这种情况是有可能发生的,A线程push了一个对象,而B线程执行autorelease时,会把A线程的对象提前释放), 即obj-autorelease().那么在time 3的时候会发生是么事情呢?答案很简单,就是obj在time 3的时候就被释放了,而我们期望它在time 4的时候才释放。所以就导致我上面说的,在多线程下面,cocos2d-x的autorelease变量会发生莫名其妙的指针异常。

  解决方法:在PoolManager给每个线程根据pthread_t的线程id生成一个CCArray的stack的嵌套管理自动释放池。在Push的时 候根据当前线程的pthread_t的线程id生成一个CCArray的stack来存储该线程对应的Autoreleasepool的嵌套对象。

虽然引擎没有为我们封装线程类,但还是提供了一些组件,辅助我们进行并发编程。除了上面提到的异步加载图片,引擎还提供了消息中心 CCNotificationCenter。这是一个类似 Qt 中消息槽的机制,一个对象可以注册到消息中心,指定要接收的消息;而某个事件完成时,则可以发送对应的消息,之前注册过的对象会得到通知。

借助消息中心,异步事件之间的对象可以进一步减少耦合,使用事件驱动的方式编写代码。

当然,在多线程的环境中,考虑到之前提到的原则,不可能直接在分离的线程中调用消息中心发送消息,我们可以建立一个线程间共享的消息池,让消息可以在不同线程间流动,或者说,我们需要建立一个线程安全的消息队列。下面我们创建一个线程安全的消息队列,代码如下:

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 = 0; 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 (0);
    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 / 2, winSize.height / 2));
    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 提供的内存管理机制基础上的,因此我们在多线程环境中使用引擎的任何对象都必须分外小心。

  • 0
    点赞
  • 0
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

鱼儿-1226

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值