cocos2d-x游戲引擎核心之八——多線程


一、多線程原理

(1)單線程的尷尬

  重新回顧下 Cocos2d-x 的並行機制。引擎內部實現了一個龐大的主循環,在每幀之間更新各個精靈的狀態、執行動作、調用定時函數等,這些操作之間可以保證嚴格獨立,互不干擾。不得不說,這是一個非常巧妙的機制,它用一個線程就實現了並發,尤其是將連續的動作變化切割為離散的狀態更新時,利用幀間間隔刷新這些狀態即實現了多個動作的模擬。

  但這在本質上畢竟是一個串行的過程,一種尷尬的場景是,我們需要執行一個大的計算任務,兩幀之間幾十毫秒的時間根本不可能完成,例如加載幾十張圖片到內存中,這時候引擎提供的 schedule 並行就顯得無力了:一次只能執行一個小時間片,我們要么將任務進一步細分為一個個更小的任務,要么只能眼睜睜地看着屏幕上的幀率往下掉,因為這個龐大計算消耗了太多時間,阻塞了主循環的正常運行。

  本來這個問題是難以避免的,但是隨着移動設備硬件性能的提高,雙核甚至四核的機器已經越來越普遍了,如果再不通過多線程挖掘硬件潛力就過於浪費了。

(2)pthead

  pthread 是一套 POSIX 標准線程庫,可以運行在各個平台上,包括 Android、iOS 和 Windows,也是 Cocos2d-x 官方推薦的多線程庫。它使用 C 語言開發,提供非常友好也足夠簡潔的開發接口。一個線程的創建通常是這樣的:

void* justAnotherTest(void *arg)
{
    LOG_FUNCTION_LIFE;
    //在這里寫入新線程將要執行的代碼
    return NULL;
}
void testThread()
{
    LOG_FUNCTION_LIFE;
    pthread_t tid;
    pthread_create(&tid, NULL, &justAnotherTest, NULL);
}

  這里我們在testThread函數中用pthread_create創建了一個線程,新線程的入口為justAnotherTest函數。pthread_create函數的代碼如下所示:

PTW32_DLLPORT int PTW32_CDECL pthread_create (pthread_t * tid,//線程的標示   
                            const pthread_attr_t * attr,      //創建線程的參數   
                            void *(*start) (void *),          //入口函數的指針   
                            void *arg);                       //傳遞給線程的數據

  pthread_create 是創建新線程的方法,它的第一個參數指定一個標識的地址,用於返回創建的線程標識;第二個參數是創建線程的參數,在不需要設置任何參數的情況下,只需傳入 NULL 即可;第三個參數則是線程入口函數的指針,被指定為 void*(void*)的形式。函數指針接受的唯一參數來源於調用 pthread_create 函數時所傳入的第四個參數,可以用於傳遞用戶數據。

(3)線程安全

  使用線程就不得不提線程安全問題。線程安全問題來源於不同線程的執行順序是不可預測的,線程調度都視系統當時的狀態而定,尤其是直接或間接的全局共享變量。如果不同線程間都存在着讀寫訪問,就很可能出現運行結果不可控的問題。

在 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 的各個接口函數也不是線程安全的。也就是說,一切和繪圖直接相關的操作都應該放在主線程內執行,而不是在新建線程內執行。(見第六點cocos2dx內存管理與多線程問題)

(4)線程間任務安排

  使用並發編程的最直接目的是保證界面流暢,這也是引擎占據主線程的原因。因此,除了界面相關的代碼外,其他操作都可以放入新的線程中執行,主要包括文件讀寫和網絡通信兩類。

  文件讀寫涉及外部存儲操作,這和內存、CPU 都不在一個響應級別上。如果將其放入主線程中,就可能會造成阻塞,尤為嚴重的是大型圖片的載入。對於碎圖壓縮后的大型紋理和高分辨率的背景圖,一次加載可能耗費 0.2 s 以上的時間,如果完全放在主線程內,會阻塞主線程相當長的時間,導致畫面停滯,游戲體驗很糟糕。在一些大型的卷軸類游戲中,這類問題尤為明顯。考慮到這個問題,Cocos2d-x 為我們提供了一個異步加載圖片的接口,不會阻塞主線程,其內部正是采用了新建線程的辦法。

  我們用游戲中的背景層為例,原來加載背景層的操作是串行的,相關代碼如下:

bool BackgroundLayer::init()
{
    LOG_FUNCTION_LIFE;
    bool bRet = false;
    do {
        CC_BREAK_IF(! CCLayer::init());
        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);
        bRet = true;
    } while (0);
    return bRet;
}

  現在我們將這一些列串行的過程分離開來,使用引擎提供的異步加載圖片接口異步加載圖片,相關代碼如下:

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后游戲中才出現了背景圖。盡管引擎已經為我們提供了異步加載圖片緩存的方式,但考慮到對圖片資源的加密解密過程是十分耗費計算資源的,我們還是有必要單開一個線程執行這一系列操作。另一個值得使用並發編程的是網絡通信。網絡通信可能比文件讀寫要慢一個數量級。一般的網絡通信庫都會提供異步傳輸形式,我們只需要注意選擇就好。

(5)線程同步

使用了線程,必然就要考慮到線程同步,不同的線程同時訪問資源的話,訪問的順序是不可預知的,會造成不可預知的結果。查看addImageAsync的實現源碼可以知道它是使用pthread_mutex_t來實現同步:

void CCTextureCache::addImageAsync(const char *path, CCObject *target, SEL_CallFuncO selector)
{
    CCAssert(path != NULL, "TextureCache: fileimage MUST not be NULL");    

    CCTexture2D *texture = NULL;

    // optimization

    std::string pathKey = path;

    pathKey = CCFileUtils::sharedFileUtils()->fullPathFromRelativePath(pathKey.c_str());
    texture = (CCTexture2D*)m_pTextures->objectForKey(pathKey.c_str());

    std::string fullpath = pathKey;
    if (texture != NULL)
    {
        if (target && selector)
        {
            (target->*selector)(texture);
        }
        
        return;
    }

    // lazy init
    if (s_pSem == NULL)
    {             
#if CC_ASYNC_TEXTURE_CACHE_USE_NAMED_SEMAPHORE
        s_pSem = sem_open(CC_ASYNC_TEXTURE_CACHE_SEMAPHORE, O_CREAT, 0644, 0);
        if( s_pSem == SEM_FAILED )
        {
            CCLOG( "CCTextureCache async thread semaphore init error: %s\n", strerror( errno ) );
            s_pSem = NULL;
            return;
        }
#else
        int semInitRet = sem_init(&s_sem, 0, 0);
        if( semInitRet < 0 )
        {
            CCLOG( "CCTextureCache async thread semaphore init error: %s\n", strerror( errno ) );
            return;
        }
        s_pSem = &s_sem;
#endif
        s_pAsyncStructQueue = new queue<AsyncStruct*>();
        s_pImageQueue = new queue<ImageInfo*>();        
        
        pthread_mutex_init(&s_asyncStructQueueMutex, NULL);
        pthread_mutex_init(&s_ImageInfoMutex, NULL);
        pthread_create(&s_loadingThread, NULL, loadImage, NULL);

        need_quit = false;
    }

    if (0 == s_nAsyncRefCount)
    {
        CCDirector::sharedDirector()->getScheduler()->scheduleSelector(schedule_selector(CCTextureCache::addImageAsyncCallBack), this, 0, false);
    }

    ++s_nAsyncRefCount;

    if (target)
    {
        target->retain();
    }

    // generate async struct
    AsyncStruct *data = new AsyncStruct();
    data->filename = fullpath.c_str();
    data->target = target;
    data->selector = selector;

    // add async struct into queue
    pthread_mutex_lock(&s_asyncStructQueueMutex);
    s_pAsyncStructQueue->push(data);
    pthread_mutex_unlock(&s_asyncStructQueueMutex);

    sem_post(s_pSem);
}

 

二、應用實例一——cococs2d-x 多線程加載plist

【轉自】 http://blog.csdn.net/we000636/article/details/8641270

(1)環境搭建

當我們想在程序中開多線程中,第一想到的是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;  
}  

(2)加載plist

  我們可以在新開的線程中,加載資源,設置一個靜態變量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渲染不兼容的問題

二、應用實例二——Cocos2d-x 3.0多線程異步資源加載

 【轉自】http://tonybai.com/2014/04/28/multithreaded-resource-loading-in-cocos2dx-3/

Cocos2d-x從2.x版本到上周剛剛才發布的Cocos2d-x 3.0 Final版,其引擎驅動核心依舊是一個單線程的“死循環”,一旦某一幀遇到了“大活兒”,比如Size很大的紋理資源加載或網絡IO或大量計算,畫面將 不可避免出現卡頓以及響應遲緩的現象。從古老的Win32 GUI編程那時起,Guru們就告訴我們:別阻塞主線程(UI線程),讓Worker線程去做那些“大活兒”吧。
 
手機游戲,即便是休閑類的小游戲,往往也涉及大量紋理資源、音視頻資源、文件讀寫以及網絡通信,處理的稍有不甚就會出現畫面卡頓,交互不暢的情況。雖然引 擎在某些方面提供了一些支持,但有些時候還是自己祭出Worker線程這個法寶比較靈活,下面就以Cocos2d-x 3.0 Final版游戲初始化為例(針對Android平台),說說如何進行多線程資源加載。
 
我們經常看到一些手機游戲,啟動之后首先會顯示一個帶有公司Logo的閃屏畫面(Flash Screen),然后才會進入一個游戲Welcome場景,點擊“開始”才正式進入游戲主場景。而這里Flash Screen的展示環節往往在后台還會做另外一件事,那就是加載游戲的圖片資源,音樂音效資源以及配置數據讀取,這算是一個“障眼法”吧,目的就是提高用 戶體驗,這樣后續場景渲染以及場景切換直接使用已經cache到內存中的數據即可,無需再行加載。
 
(1)為游戲添加FlashScene
在游戲App初始化時,我們首先創建FlashScene,讓游戲盡快顯示FlashScene畫面:
// AppDelegate.cpp 
bool AppDelegate::applicationDidFinishLaunching() { 
    … … 
    FlashScene* scene = FlashScene::create(); 
    pDirector->runWithScene(scene); 
  
    return true; 
} 

在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一下試試吧。

(2)崩潰
在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.
 
(3) 黑屏

上面的代碼成功解決了線程崩潰的問題,但問題還沒完,因為接下來我們又遇到了“黑屏”事件。所謂的“黑屏”,其實並不是全黑。但進入游戲 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,我們來修改一下代碼:
static void* 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()); 
    } 
} 

完成這一修改后,游戲畫面就變得一切正常了,多線程資源加載機制正式生效。

------------------------------------------------------------------------------------------------

CocoaChina是全球最大的蘋果開發中文社區.

 

 (6)cocos2dx內存管理與多線程問題

【轉自】http://blog.csdn.net/kaitiren/article/details/14453313

  Cocos2d-x的內存管理采用Objective-C的機制,大喜過望。因為只要堅持Objective-C的原則“誰創建誰釋放,誰備份誰釋放”的原則即可確保內存使用不易出現Bug。
   但是因為游戲需要使用到多線程技術,導致測試的時候總是莫名其妙的導致空指針錯誤。而且是隨機出現,糾結了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的嵌套對象。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM