cocos2d-x遊戲引擎核心之多線程分析及注意事項

轉載自:http://www.cnblogs.com/yyxt/p/4087123.html

一、多線程原理

(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的嵌套對象。


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 = 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 提供的內存管理機制基礎上的,因此我們在多線程環境中使用引擎的任何對象都必須分外小心。

二、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被調用的,也就是整個函數體並沒結束,數據不會被銷燬。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章