wordcount設計與優化

原文檔見:http://gitlab.alibaba-inc.com/middleware/coding4fun-3rd/blob/master/observer.hany/design.mdnode

  • 淘寶中間件第三期編程比賽,題意概述:讀入一個文件,統計其中最常出現的前10個單詞。git

系統設計

  • 按照題意,可設計以下簡單拓撲圖。220819277.png程序員

    • 圖中方塊表示計算節點箭頭表示數據流動
      注意:Counter和Selector之間須要設置一道柵欄,全部單詞統計完畢後才能開始篩選單詞。編程

    優化1:同步OR異步

    • Reader是IO集中型操做,其餘計算節點都是CPU集中型操做。
      若是先讀完文件再操做,讀文件的這段時間CPU就白白空閒着浪費掉了。安全

    • 簡單的優化就是異步讀文件。
      增長一個後臺Task線程,Reader每讀取一小塊文件數據(Chunk),
      就交給Task線程處理,Reader繼續讀下一個Chunk的時候,Task已經跑起來了,
      一個佔用IO,一個佔用CPU,充分利用計算機資源。併發

220855277.png

經過異步讀文件,Reader和Task可以併發處理數據,提升性能。實現細節ChunkedTextReader實現按Chunk分塊讀文件。爲了不Chunk邊界意外將一個單詞拆成兩半,除最後一個Chunk外,每一個Chunk都將末尾的最後一個單詞切開,拼接到下一個Chunk的前面,讓下一個Chunk處理。Reader和Task之間經過BlockingQueue傳輸數據,這是一個線程安全的「生產者-消費者」隊列。經測試,Chunk分塊過小隊列操做過於頻繁,性能降低。分塊太大讀文件阻塞過久,達不到異步讀的目的,所以默認限制Chunk最小1MB,最大8MB。優化2:併發OR併發讀文件的速度比處理文件的速度快的多,一個線程CPU跑到100%也是遠遠處理不過來。測試機有16個核,可建立多個併發的Task線程,將每一個核都利用起來。因爲Task是高度CPU密集型操做,默認取Task線程數等於CPU核數。柵欄控制全部數據處理完成才能開始按詞頻選擇單詞。實現細節ConcurrentBlockingQueueExecutor管理全部Task線程,executor在每一個線程上等待線程結束,實現柵欄同步。ConcurrentBlockingQueueTask實現Task線程處理流程。Reader讀完文件後在executor上設置done標識位,Task發現queue爲空且executor設置了done標誌位,則說明文件已經讀完並處理完,task結束。ConcurrentTrieNode實現了線程安全的Trie樹。語言細節在Java實現中,ConcurrentBlockingQueueTask和ConcurrentBlockingQueueExecutor互相依賴,但C++不能處理互相依賴,因此將task對executor的依賴剝離到ConcurrentBlockingQueueExecutorSupport中,避免互相依賴的問題。C++程序員一般使用前置聲明、分離實現等辦法解決互相依賴問題。程序先用Java設計開發完成,再逐個類翻譯成C++。編碼儘可能遵照Java約定,每一個類放到獨立的文件,方法實現直接寫在頭文件的類聲明中,」.cpp」文件基本都是空的。排除「.cpp」文件,文件數量就少一半了,嘿嘿~~簡單場景還能用C++模擬一下Java,複雜場景就只能用Java了。優化3:雙保險模式避免加鎖DANGEROUS雙保險模式已經被證實是不可靠的,禁止在生產代碼中使用。UPDATE@齊楠@宏江指出,jdk1.5以後加上volatile關鍵字雙保險模式是可用的。早期版本不行。ConcurrentTrieNode* getChild(char c) { int const index = c - 'a'; if (children[index] == NULL) { synchronized: { Locker locker(childrenLock); if (children[index] == NULL) { children[index] = (ConcurrentTrieNode*) calloc(1, sizeof(ConcurrentTrieNode)); } } } return children[index]; }語言細節ConcurrentTrieNode是一個簡單struct,不包含虛函數和複雜對象字段,其構造函數只是簡單地將全部字段(包括Lock)初始化爲0。使用calloc(1,sizeof(ConcurrentTrieNode))直接分配一塊0初始化的內存,calloc返回內存地址時,已經獲得一個合法初始化的ConcurrentTrieNode對象,而沒必要調用構造函數。優化4:原子操做避免加鎖併發統計count時,將count字段聲明爲volatile(),/** * Word 出現的次數. */ volatile int count;使用原子操做實現線程安全並避免加鎖(),提升性能。// atomic_inc __asm__ __volatile__( "lock ; " "incl %0" :"=m" (node->count) :"m" (node->count));優化5:統計單詞結束後再過濾排除單詞程序要求排除一些單詞,在統計前排除,每一個分詞都要判斷一次。統計結束再排除,相同單詞已經合併,減小判斷,性能更高。總結優化過程當中曾想過各類方案,好比並發mergesort排序再處理,每線程一個Map最後再合併等,結果發現使用ConcurrentHashMap不但編程複雜度明顯簡單,性能還更加理想。再一次證實,最簡單的方案每每就是最好的方案,不只從開發維護的角度來看,有時從性能角度來看也是這樣。Java的ConcurrentHashMap性能至關贊,併發環境首選啊。程序開始是用JavaConcurrentHashMap實現的。爲了提高性能翻譯成C++,過程可謂大費周折,至關痛苦,我會告訴你我大半夜還在調segmentalfault嗎?C++沒有ConcurrentHashMap,實現ConcurrentTrie相對簡單,因此選擇了Trie。不少同窗採用Java實現性能也很是好,至關贊!
相關文章
相關標籤/搜索