Chrome源碼——執行緒模式

來源:互聯網
上載者:User
很多人喜歡Chrome,喜歡它的簡潔,喜歡它的“快”。 簡潔大家一目瞭然,無需多言,這裡重點要說的是它的“快”。什麼是“快”呢。大概不少人第一反應就是cnBeta上的日經Javascript跑分貼,Chrome那叫一個快啊。(其實每次點開這類文章時,我都是想去同情一下我們可憐的IE同學的,不過最近IE9著實又讓人充滿了期待。)但Javascript跑分真的這麼重要嗎,我們真的能體會到很大的差距嗎。反正我是感覺不太出來。那憑啥還說Chrome快呢。Chrome快就快在UI Responsive(UI響應性)上。說白了就是你雙擊一下案頭表徵圖,它能很快啟動起來(Firefox使用者應該深有感觸吧);點擊某個超連結,它能瞬間給你彈出個新頁面來(用IE的朋友也沒少遇過假死的現象吧)。因此,Chrome並不只是個跑分的工具。那Chrome良好的UI Responsive是如何做到的呢。這就要得益於它的執行緒模式了。

  為了避免重複製造輪子,這裡我並不打算闡述Chrome執行緒模式的總體架構,而是將更多的精力放在具體實現上。對Chrome執行緒模式架構不熟悉的朋友可以閱讀一下duguguiyu的這篇文章。有條件的朋友可以到這裡去下載一份Chromium的源碼,或者到這裡線上查看。閑話少說, Let's Go!

 

NO.1 Smart Pointers 

整個Chromium源碼應該有十幾萬個檔案吧,面對如此浩瀚的code海,我們該從哪下手呢。古話說,工欲善其事,必先利其器。我們就從Chromium中用得最多的幾個智能指標著手吧。

先看scoped_ptr(可參見base/scoped_ptr.h)。這是一個在析構時將自動刪除所持有對象的智能指標。其行為和std::tr1::scoped_ptr基本一致,不予多說。

Chromium中一個比較有意思的常用指標是scoped_refptr(可參見base/ref_counted.h)。和scoped_ptr稍微有點區別,scoped_refptr只能引用顯式實現了引用記數介面的類型: 代碼 template  < class  T >
class  scoped_refptr {
  public :
  scoped_refptr(T *  p) : ptr_(p) {
     if  (ptr_)
      ptr_ -> AddRef();
  }
   ~ scoped_refptr() {
     if  (ptr_)
      ptr_ -> Release();
  }
  protected :
  T *  ptr_;
}; 複製代碼

   如上所示,class T必須顯式實現AddRef和Release介面,而scoped_refptr在構造和析構的時候將調用它們來增加/減少引用記數。當一個對象引用記數為0時,class T的Release方法將負責將該對象自動刪除。 很多人禁不住要問了,那要寫一個class T豈不是很麻煩。別擔心,Google已經為我們提供了兩個預設的實現:RefCounted和RefCountedThreadSafe(均在base/ref_counted.h中),我們可以通過繼承它們來讓我們的類實現引用記數功能。

RefCounted比較簡單,但值得注意的一點,它並不保證執行緒安全性,因此只能應用於單線程持有的對象。如果需要在多個線程中都可以引用的對象時,該怎麼辦呢。這時RefCountedThreadSafe就派上用場了(個人並不喜歡這個名字,或許叫ThreadSafeRefCounted會更合適)。 代碼 template  < class  T, typename Traits  =  DefaultRefCountedThreadSafeTraits < T >   >
class  RefCountedThreadSafe :  public  subtle::RefCountedThreadSafeBase {
  public :
   void  AddRef() {
    subtle::RefCountedThreadSafeBase::AddRef();
  }
   void  Release() {
     if  (subtle::RefCountedThreadSafeBase::Release()) {
      Traits::Destruct(static_cast < T *> ( this ));
    }
  }
};
void  RefCountedThreadSafeBase::AddRef() {
  AtomicRefCountInc( & ref_count_);
}
複製代碼

  我們可以看到,RefCountedThreadSafe在增加和刪除引用技術時,都採用了原子操作,保證了執行緒安全性。

當然,如果你認為RefCountedThreadSafe就這麼簡單的話,你就低估它了。仔細看,在類型執行個體化的時候,除了傳入了一個class T,還有個Traits。那這個Traits是派什麼用場的呢。注意Release方法,當引用記數為0時,正是使用了Traits來負責刪除該對象。這就給了我們一個自訂刪除對象的機會,這在很多情況下是非常有用的。比如說,一般來說,很多UI對象是不能在主線程之外析構的。一旦我們把它交給了scoped_refptr來管理,我們就無法保證它會在哪個線程中引用記數被置0,而這時需要做的只是使用自訂的Traits來給主線程發一個訊息,讓主線程負責去刪除該對象,這就避免了很多問題的產生。

寫到這裡,不禁想稍微比較一下scoped_refptr和std::tr1::shared_ptr。同樣使用引用記數來實現對象的自動刪除,scoped_refptr卻要求所引用的對象都顯式實現AddRef和Release介面,還需要該對象的解構函式為私人,並把scoped_refptr設為友元;而shared_ptr只要求該對象提供一個public的解構函式就可以了。一眼看上去,似乎shared_ptr優雅很多。但正是這種表面上的優雅,讓很多人在有意和無意間很容易忽略該對象可能會在多線程的環境中被使用,從而給我們帶來了無數的煩惱。相比較這種優雅,我個人還是比較欣賞scoped_refptr這種有著明確語義,而且不容易引起誤用的設計。另外還有一點就是上文所說的Traits,這是個非常實用的功能。可惜的是,如果我們把對象交付給了shared_ptr,就沒辦法再去控制它的析構了。如果不是看到這段代碼,或許我根本就不會想到,原來shared_ptr還有這麼多不盡如人意的地方,所以也強烈推薦大家多去讀讀這樣高品質的代碼。

另外還有一個指標是WeakPtr(base/weak_ptr.h),這裡就不多做描述了,需要瞭解的朋友可以自行閱讀它的代碼。 

 

No.2 Tasks 

有了智能指標的知識做基礎,我們接下來就來看看Chrome中Task的實現了。

Task類(base/task.h)的封裝很簡單,如下: class  Task :  public  tracked_objects::Tracked {
  public :
   virtual   ~ Task();
   //  Tasks are automatically deleted after Run is called.
   virtual   void  Run()  =   0 ;
};

  稍微可以說下的是它所繼承的Tracked類,這是用來做調試和效能跟蹤用的。它會記錄Task產生和銷毀的時間,以及在何處被Post到線程的訊息迴圈中去的(通過__LINE__, __FILE__等編譯器定義的宏)。更多細節可以參閱base/tracked.h及相關檔案。另外一些諸如CancelableTask、DeleteTask等子介面和子類也都是小角色,沒什麼可多說的。

看了上面的實現,很多人可能會問:如果每次我想往其他線程分發一個任務,都要寫一個類來實現Task介面,豈不是很煩。如果你也有一樣的憂慮,那麼恭喜你,你和Chromium的設計者想到一塊去了。task.h中還提供了很多NewRunnableMethod方法來讓我們可以輕鬆的把用一個對象和它的方法來構造Task。 代碼 template  < class  T,  class  Method,  class  A >
inline CancelableTask *  NewRunnableMethod(T *   object , Method method,  const  A &  a) {
   return   new  RunnableMethod < T, Method, Tuple1 < A >   > ( object , method, MakeTuple(a));
}

template  < class  T,  class  Method,  class  Params >
class  RunnableMethod :  public  CancelableTask {
  public :
  RunnableMethod(T *  obj, Method meth,  const  Params &   params )
      : obj_(obj), meth_(meth), params_( params ) {
    traits_.RetainCallee(obj_);
    COMPILE_ASSERT((MethodUsesScopedRefptrCorrectly < Method, Params > ::value),
                   badrunnablemethodparams);
  }
  private :
  T *  obj_;
  Method meth_;
  Params params_;
  RunnableMethodTraits < T >  traits_;
};
複製代碼

  注意這句:COMPILE_ASSERT((MethodUsesScopedRefptrCorrectly<Method, Params>::value), badrunnablemethodparams)。這句的意思是保證所有傳入Method的指標類型的參數都是scoped_refptr,否則編譯器將報錯。(關於如何?編譯期Assert,可以自行查閱源碼或參考《Modern C++ Design》。)細心的朋友可能還會發現:這裡怎麼也有個Traits。正像它名字描述的那樣,所有Traits其實都只是一些小把戲。RunnableMethodTraits無非是在涉及多線程時,對Class T是否實現了RefCountedThreadSafe介面等做一些檢查(只在Debug版本中檢查)。另外,傳入Method的方法參數將被拷貝(指標拷貝的是值,而不是指向的對象),如果你有一些自訂類型要使用特殊的拷貝語義,可以自己去對TrupleTraits做template specialization。

有了這些,我們的Task實現是不是就完備了呢。答案顯然是否定的。我們很可能會用NewRunnableMethod(new T, &T::SomeMethod)來構建了一個Task並把它分發到指定線程。但問題是,如果在該Task執行之前,new T所建立出來的對象已經被銷毀了呢。那Task所持有的不就成了一個Pending Pointer,這不就會引發Access Violation這樣嚴重的問題了。所以Chromium給我們提供了另一套解決方案:Scoped factories。本著不重複製造輪子的原則,願意做詳細瞭解的朋友請閱讀Optman的這篇文章。

 

No.3 Cross-Thread Request

  Chrome中所有線程都有著明確的分工,比如UI線程專門負責響應使用者請求,File線程專門負責訪問檔案系統。

一種很自然的情況是,使用者可能會想查看某個檔案的內容。在Chrome中,這項任務的完成需要UI線程先往File線程的訊息佇列中添加一個讀取檔案的Task,然後該Task才會在File線程中執行(如果File線程中還有其他Task,可能還需要等待)。等這個Task執行完之後,再往UI線程的訊息佇列裡添加另一個Task用於顯示檔案的具體內容。等到顯示檔案內容的Task執行完後,該檔案才被顯示出來。看起來很複雜吧。而且在這些過程當中,也沒有辦法保證所有線程都沒有被析構。當然,設計者是不可能把這麼複雜的功能直接暴露給使用者的。CancelableRequest類就是用來封裝這種複雜性的。再次感謝Optman,他的這篇文章為我們詳細闡述了CancelableRequest類的實現。

但是,如果我們只是知道如何去實現,而不去關心為什麼要這樣做的話,也就成了聖人所說的學而不思者,後果必然就是“罔”了。不覺得相對於在UI線程中直接去讀取檔案,Chrome折騰這麼一大堆東西之後,反而要多出很多線程調度和Task排隊等待的時間來嗎。那它是不是在瞎折騰呢。細細品味,這完全是有道理的。正是因為這個設計,才使得Chrome擁有了優秀的UI Responsive。我們知道,UI無響應的罪魁禍首就是在於在UI線程中執行耗時的操作。在UI線程中讀取檔案,帶來的最直接後果就是使用者容易覺得卡。也許你會質疑,如果我讀的只是一個很小的檔案呢,這樣的話使用者應該完全感覺不到啊。但是,如果這個時候你的卡巴斯基在“喀喀喀”地掃描你的硬碟呢,你的程式在這種極端的硬碟IO下面還能保持良好的響應性嗎。再看Chrome,無論你硬碟怎麼“喀”,UI線程只要在往File線程添加一個Task後就返回去響應使用者操作了,而File線程讀好之後,也能很快地進行更新。或許從使用者操作到真正顯示出檔案內容來的時間上,Chrome還是要長些,但在這段時間中,使用者還是能夠流暢地切換標籤頁、開啟新頁面等。但如果使用UI線程直接存取檔案,這就不可能做到了。

 

No.4 Sample

雖然還有MessageLoop、Thread等類沒說,但已經不妨礙我們來看懂實際的例子了。可能很多人還是有疑問,在Chrome的執行緒模式中,到底是怎樣規避使用鎖的。這就要得益於它在對象設計時地規定對象只能生存在一個線程中。可能這麼說還是不好理解。有句老話說,依葫蘆畫瓢。現在我們就把一個葫蘆拿出來瞅瞅到底長啥樣。 接下來將以Chrome的記錄管理做為例子來具體描述。(具體的代碼請參見chrome/browser/history目錄下的history.h和history.cc。)

  history.h中有一個重要的類HistoryService,它是運行在UI線程中並給UI提供服務的。它有兩個重要的成員thread_和history_backend_。thread_是用來初始化一個後台訪問記錄的線程(History線程),而history_backend_則在這個線程上勤勤懇懇地工作,幫HistoryService完成真正的資料訪問。這就是所謂的對象只能生存在一個線程中:HistoryService生存在UI線程中,而history_backend_則生存在History線程中。請注意history_backend_是如何初始化和刪除的。 代碼 class  HistoryService :  public  CancelableRequestProvider,
                        public  NotificationObserver,
                        public   base ::RefCountedThreadSafe < HistoryService >  {
  .........
  private :
  scoped_refptr < history::HistoryBackend >  history_backend_;
   base ::Thread *  thread_;
};

HistoryService::HistoryService()
    : thread_( new   base ::Thread(kHistoryThreadName)) ... {
  ......
}

bool  HistoryService::Init(...) {
   if  ( ! thread_ -> Start()) {
    Cleanup();
     return   false ;
  }
  ......
   //  Create the history backend.
  LoadBackendIfNecessary();
   return   true ;
}

void  HistoryService::LoadBackendIfNecessary() {
   if  ( ! thread_  ||  history_backend_)
     return ;   //  Failed to init, or already started loading.

  scoped_refptr < HistoryBackend >  backend( new  HistoryBackend(...));
  history_backend_.swap(backend);

  ScheduleAndForget(PRIORITY_UI,  & HistoryBackend::Init, no_db_);
}

HistoryService:: ~ HistoryService() {
  Cleanup();
}

void  HistoryService::Cleanup() {
   if  ( ! thread_) {
     //  We've already cleaned up.
     return ;
  }
   //  Unload the backend.
  UnloadBackend();
   base ::Thread *  thread  =  thread_;
  thread_  =  NULL;
  delete thread;
}

void  HistoryService::UnloadBackend() {
   if  ( ! history_backend_)
     return ;   //  Already unloaded.
  Task *  closing_task  =  NewRunnableMethod(history_backend_. get (),  & HistoryBackend::Closing);
  history_backend_  =  NULL;
  ScheduleTask(PRIORITY_NORMAL, closing_task);
}

複製代碼

   需要說明的是,history_backend_構造時只是簡單地把所有指標成員置NULL而沒有具體的初始化動作(HistoryBackend::Init方法),這既保證了UI線程在構造HistoryService時也可以快速完成而不需要等待,同時也是因為它需要在thread_所代表的線程中來完成初始化。當HistoryService需要調用HistoryBackend的方法時,也是通過往thread_的訊息迴圈上分發Task來非同步完成的。關於HistoryService是如何往thread_上分發任務和發起Cross-Thread Request的請自行參閱代碼,基本都是使用了上面提到的方法,比較好懂。


      扯了這麼多,該小結一下了。好像也沒什麼特別的感慨要發,只是覺得這些代碼確實品質很高而且很精妙,稍加整理也可以用於我們自己的工程中。當然,請記得一定要遵循開源的遊戲規則。想進一步瞭解的朋友可以自己去研讀代碼。本人水平有限,上面描述中有不妥當的地方,也請不吝指出。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.