[cocos2d-x]CCHttpClient的一個bug,cchttpclient
公司的新遊戲《我是大官人》馬上就要大規模PR了,一切都已經準備就緒,這時測試部門卻反饋了一個小問題,開啟遊戲的時候,偶爾會卡在啟動介面,提示:正在串連伺服器...然後就沒反應了,這個問題發生的機率很低,大概3%左右,而且退出重新開啟遊戲就好了,“應該是網路不好造成的”,大家並沒有太重視這個bug,但是老闆不放心,“就算是網路問題,也不應該卡住,如果是新玩家碰到這種情況就直接流失了,這個問題得查一下。” 看來這不是一個小問題,於是這個bug分配給了我。
花了一些時間重現,我發現問題出在CCHttpClient,熟悉cocos2d-x的同學知道,這是一個非同步網路程式庫,它的工作原理是這樣的:HttpClient內部有一個背景工作執行緒,遊戲主線程調用send()函數,將http請求壓入一個隊列,然後喚醒背景工作執行緒就返回了。背景工作執行緒被喚醒後,將http請求從隊列中取出,再調用libcurl進行處理,然後再通過回調將結果返回主線程。bug重現的時候,主線程註冊的回調沒有觸發,遊戲就卡住了。我估計是libcurl將背景工作執行緒卡住了,於是在背景工作執行緒中加入了一些列印進行驗證,結果卻大意料:背景工作執行緒根本就沒有觸發!
又是一個多線程同步的問題!這是背景工作執行緒的程式碼片段:
// Worker threadstatic void* networkThread(void *data){ CCHttpRequest *request = NULL; while (true) { if (need_quit) { break; } // step 1: send http request if the requestQueue isn't empty request = NULL; pthread_mutex_lock(&s_requestQueueMutex); //Get request task from queue if (0 != s_requestQueue->count()) { request = dynamic_cast<CCHttpRequest*>(s_requestQueue->objectAtIndex(0)); s_requestQueue->removeObjectAtIndex(0); // request's refcount = 1 here } pthread_mutex_unlock(&s_requestQueueMutex); if (NULL == request) { // Wait for http request tasks from main thread pthread_cond_wait(&s_SleepCondition, &s_SleepMutex); continue; } // step 2: libcurl sync access
背景工作執行緒閒置時候,通過pthread_cond_wait()掛起
send()函數的代碼:
//Add a get task to queuevoid CCHttpClient::send(CCHttpRequest* request){ if (false == lazyInitThreadSemphore()) { return; } if (!request) { return; } ++s_asyncRequestCount; request->retain(); pthread_mutex_lock(&s_requestQueueMutex); s_requestQueue->addObject(request); pthread_mutex_unlock(&s_requestQueueMutex); // Notify thread start to work pthread_cond_signal(&s_SleepCondition);}
我仔細的看了一遍,發現這句話很奇怪:
pthread_cond_wait(&s_SleepCondition, &s_SleepMutex);
這裡用了一個互斥鎖s_SleepMutex,而這個鎖沒有其他代碼使用。這很可能是一個錯誤:一個互斥鎖如果只有一處代碼使用,那麼只有一種可能,這段代碼會在多個線程中執行。而httpClient的線程顯然只有一個。google了一番,果然是這個互斥鎖用錯了。大家可以參考知乎上的一篇文章:http://www.zhihu.com/question/24116967
裡面詳細解釋了為什麼沒有正確的加鎖會導致訊號量丟失。
這裡是修改後的代碼,不再使用單獨的s_SleepMutex, 用requestQueueMutex代替
// Worker threadstatic void* networkThread(void *data){ CCHttpRequest *request = NULL; while (true) { if (need_quit) { break; } // step 1: send http request if the requestQueue isn't empty request = NULL; pthread_mutex_lock(&s_requestQueueMutex); //Get request task from queue while (0 == s_requestQueue->count()) { pthread_cond_wait(&s_SleepCondition, &s_requestQueueMutex); } request = dynamic_cast<CCHttpRequest*>(s_requestQueue->objectAtIndex(0)); s_requestQueue->removeObjectAtIndex(0); // request's refcount = 1 here CCLog("s_SleepCondition notified"); pthread_mutex_unlock(&s_requestQueueMutex);
反覆測試了一個下午,這個bug沒有再複現了。
這是在cocos2d-x 2.1.4版本上改的,cocos2d-x 3.x沒有使用pthread_cond_wait進行線程同步,但也存在類似的問題。