1. 程式人生 > >一個詭異BUG引發的血案(執行緒死鎖造成的CPU利用率逐漸增高)

一個詭異BUG引發的血案(執行緒死鎖造成的CPU利用率逐漸增高)

        我首先宣告,我有一些標題黨的嫌疑,確實沒有什麼血案發生,頂多是我找BUG 時由於太用力把嘴脣給咬破了。

        這個故事發生在剛剛過去的猴年春節前夕,公司準備上傳最後一個包到appstore,然後大家各回各家各找各媽。大家憧憬著即將到來的假期寫著程式碼,以完成最後一個版本。由於這時候程式猿的春天即將到來,內心總是蠢蠢欲動的,程式碼自然而然的寫的有點糙。在測試即將完成進入效能測試步驟時,發現手機在使用一段時間後會發燙,並且程式卡頓的要死。由於這種現象是在一段時間後才發生的,所以一開始誰也沒有在意,當問題暴漏的時候,已經距離發版日期很近了,也就是說如果我們不能在一週內解決那就要放棄這個版本,或者放棄過春節。

        小夥伴們都懵逼了,大家開會自查程式碼,優化效能,利用instument裡的找問題。發現了很多程式碼寫的不合理的地方,一一改正,這裡我必須說一下,作為一個程式猿是要有猿類的覺悟的,可能我們生活中不太注意形象,作風邋遢隨性,形象無法登大雅之堂,但是程式碼從我們手中寫出來絕對不能亂七八糟,敷衍了事,相信每一個自律的程式設計師對自己的程式碼都有一定的要求,程式碼一定要遵從自己心胸中認定的最熟悉的規範。千萬不能有個僥倖心理什麼的,有個定律叫墨菲定律,意思是該發生的一定會發,出來混遲早要還的。

你少寫一個判斷,那麼一定會有崩潰發生在哪裡,影響著軟體的質量。如果你少寫兩行註釋,那麼很久以後當你自己也讀不懂這些程式碼的時候會從心裡鄙視“他*的,這誰寫的程式碼,這麼亂”,全然想不起自己鄙視了自己一番。

         說太多了,乾貨在這裡。我們優化了許多程式碼以後,雖然有些改善,但是軟體過一段時間後依然會卡頓無比,奇怪的是記憶體並未上漲,多次檢查過發現並沒有記憶體洩漏。

用instument裡的 time profile工具檢視發現CPU的佔用率是隨時間增加而逐漸上升的,於是小夥伴們一看不是常見問題紛紛束手無策。這個時候作為一個團隊拿工資最高的傢伙,解決這些疑難問題簡直是義不容辭啊。於是我開始瞭解決問題的漫漫征程,之所以說稱之為漫漫是因為解決問題一共花了3天,加上期間公司年會和部門年會的兩天無心幹活的話,那一共花了5天。

        我思考了一下問題表現和可能發生的情況,發現這種情況app的記憶體不漲,CPU逐漸升高,那麼問題一定是出在執行緒上,利用工具檢查後印證了我的想法,我發現程式的執行緒數一直在漲漲漲,執行緒數一多,CPU處理主要邏輯時wait 的時間就會變長,cpu的運算被浪費在等待那些無用的增多的執行緒中去了。但是無奈的是我怎麼看怎麼看不出到底為何會漲。於是我要求小夥伴們自查程式碼以及互相review程式碼,看看有沒有濫用執行緒的情況,查了整整一天直到晚上11點,最終無果。

         第二天我無奈的發現我只有一種方法來找到我想要的東西,當所有的辦法都不管用,那就只剩下排除法了。因為上一個版本還沒有類似的問題,那麼我只要註釋掉這次迭代的程式碼,一點一點地開啟註釋,用排除法找到出問題的程式碼。悲催的是BUG雖然是必現的但是由於每次需要半小時才能出現明顯的卡頓現象,那麼排除法查詢問題就變的奇慢無比,這真的是一種折磨啊!最後春節將至,小夥伴們開始陸續提前放假,我只好一個人孤獨的尋找問題。

          第三天當我找到出問題的程式碼後,我無論如何也不能相信,這段程式碼可以導致CPU利用率過高,而且這段程式碼根本看不出和執行緒有什麼關係。我把程式碼貼出來就在下面,當時我第一反應是隨機數函式和獲取時間戳程式碼會耗時麼?難道編譯器出問題了?於是我又寫了個Demo把這段程式碼分離出來測試,發現居然沒有任何問題,不會出現任何導致CPU利用率上升的問題,而且他*的根本沒用執行緒啊?

-(void)resetTimeSign
{
    NSString * Factor =[NetConfiguration sharedNetConfig].factor;
    NSString* timeStamp=[NSString stringWithFormat:@"%.0f",[NSDate date].timeIntervalSince1970*1000];
    NSString *text= [NSString stringWithFormat:@"%@_%@_%@",Factor,self.userItem.userID,timeStamp];
    [[NetConfiguration sharedNetConfig] addCommonRequestHeaderValue:timeStamp forKey:kDefault_Cleartext];
    int arc =arc4random()%900+100;
    NSString *cipherText = [NSString stringWithFormat:@"%d%@%d",arc,[[NetConfiguration sharedNetConfig] getReverseString:text],arc];
    [[NetConfiguration sharedNetConfig] addCommonRequestHeaderValue:cipherText forKey:kDefault_Ciphertext];//--cyc
}
         當我意識到可能是這個單列的問題時已經是第四天了,這時候年會過完,抽獎抽到了一個鍵盤,心說果然是程式猿的命啊!藉著好心情我又分析了一下,這幾行程式碼,如果有問題,肯定在這幾個單列裡。於是我終於開始從頭看這個單列,看了一眼,頓時開始問候無數次某人女性先祖,單列是這麼寫的
+ (UserManager *)shareUserManager
{
    if (shareUserManager != nil) {
        return shareUserManager;
    }
    @synchronized(self)
    {
        if (!shareUserManager) {
            shareUserManager = [[UserManager alloc] init];
        }
    }
    return shareUserManager;
}

          當看到@synchronized的時候,我終於明白出了啥事情,我們有個某執行緒輪詢操作,每次都生成一條新執行緒,然後銷燬,然而這個東西一呼叫和另一處鎖造成了死鎖,於是執行緒越來越多,無法銷燬...... 死鎖的執行緒參與分配CPU時間片,於是主執行緒wait的時間越來越多,越來越卡。

          這年頭單列早就不這麼寫了好吧,單列如今怎麼寫我也不在這裡鰲述,這次的問題解決後我明白了一個道理,工具並不總是有用的,有時候走投無路的時候,排除法這種笨辦法反而能解決問題,而且為了快速找到問題,我借鑑了二分查詢的思想來做排除法,覺得能解決問題的辦法就是好辦法。總之黑貓白貓,能逮到老鼠就是好貓。當然解決問題一定有更好的辦法,如果你看了以後覺得有好辦法,希望大神可以不吝賜教!