1、多線程原理android
(1)單線程的尷尬編程
從新回顧下 Cocos2d-x 的並行機制。引擎內部實現了一個龐大的主循環,在每幀之間更新各個精靈的狀態、執行動做、調用定時函數等,這些操做之間能夠保證嚴格獨立,互不干擾。不得不說,這是一個很是巧妙的機制,它用一個線程就實現了併發,尤爲是將連續的動做變化切割爲離散的狀態更新時,利用幀間間隔刷新這些狀態即實現了多個動做的模擬。windows
但這在本質上畢竟是一個串行的過程,一種尷尬的場景是,咱們須要執行一個大的計算任務,兩幀之間幾十毫秒的時間根本不可能完成,例如加載幾十張圖片到內存中,這時候引擎提供的 schedule 並行就顯得無力了:一次只能執行一個小時間片,咱們要麼將任務進一步細分爲一個個更小的任務,要麼只能眼睜睜地看着屏幕上的幀率往下掉,由於這個龐大計算消耗了太多時間,阻塞了主循環的正常運行。api
原本這個問題是難以免的,可是隨着移動設備硬件性能的提升,雙核甚至四核的機器已經愈來愈廣泛了,若是再不經過多線程挖掘硬件潛力就過於浪費了。緩存
(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)線程安全app
使用線程就不得不提線程安全問題。線程安全問題來源於不一樣線程的執行順序是不可預測的,線程調度都視系統當時的狀態而定,尤爲是直接或間接的全局共享變量。若是不一樣線程間都存在着讀寫訪問,就極可能出現運行結果不可控的問題。
在 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); }
2、應用實例一——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渲染不兼容的問題
2、應用實例二——Cocos2d-x 3.0多線程異步資源加載
【轉自】http://tonybai.com/2014/04/28/multithreaded-resource-loading-in-cocos2dx-3/
// 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一下試試吧。
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);
#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.
上面的代碼成功解決了線程崩潰的問題,但問題還沒完,由於接下來咱們又遇到了「黑屏」事件。所謂的「黑屏」,其實並非全黑。但進入遊戲 WelcomScene時,只有Scene中的LabelTTF實例能顯示出來,其他Sprite都沒法顯示。顯然確定與咱們在Worker線程加載紋理資源有關了:
libEGL: call to OpenGL ES API with no current context (logged once per thread)
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()); } }
完成這一修改後,遊戲畫面就變得一切正常了,多線程資源加載機制正式生效。
------------------------------------------------------------------------------------------------
(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的嵌套對象。