您好,登錄后才能下訂單哦!
線程局部存儲(Thread Local Storage), 簡稱TLS,提供了一種存儲線程私有數據的方式,每個線程的私有數據對其他線程均不可見。Chromium是一個多進程多線程架構的瀏覽器,運行時會創建多達30幾個線程,其中很多線程需要擁有自己私有數據,在TLS數量有限的系統上,例如Android 4.3或更早的系統,可能會因為無法分配足夠的TLS而導致Chromium崩潰。本文將介紹最近在Chromium提交代碼中是如何解決這個問題的。
Chromium代碼中,有些類提供了一個current()方法用來返回調用線程的私有數據,最典型的兩個例子是MessageLoop和RenderThread。以MessageLoop為例,因為每個線程可能運行著一個主消息循環,如何能夠獲取與線程相關的主消息循環呢?為此,MessageLoop提供了current()方法,當線程在創建MessageLoop實例時,會將這個實例設置為線程的私有數據,當調用current()方法時,再將這個私有數據返回給調用者。
每個TLS槽都由一個唯一的鍵值(key)標識,這個鍵值由進程負責向OS申請分配,如果當前進程申請的鍵值數量超過系統規定的上限,申請將失敗,即無法獲取一個新的TLS槽。線程可以將TLS槽綁定到一個私有數據上,這個私有數據實際上是一個指針,指向一塊由調用線程動態分配的內存塊。
Chromium代碼中,對TLS的使用都抽象在ThreadLocalPointer模板類中(參見base/threading/thread_local.h文件):
template <typename Type> class ThreadLocalPointer { public: ThreadLocalPointer() : slot_() { internal::ThreadLocalPlatform::AllocateSlot(slot_); } ~ThreadLocalPointer() { internal::ThreadLocalPlatform::FreeSlot(slot_); } Type* Get() { return static_cast<Type*>( internal::ThreadLocalPlatform::GetValueFromSlot(slot_)); } void Set(Type* ptr) { internal::ThreadLocalPlatform::SetValueInSlot( slot_, const_cast<void*>(static_cast<const void*>(ptr))); } private: typedef internal::ThreadLocalPlatform::SlotType SlotType; SlotType slot_; DISALLOW_COPY_AND_ASSIGN(ThreadLocalPointer<Type>); };
其中SlotType是對TLS槽(鍵值)類型的抽象,internal::ThreadLocalPlatform是對特定平臺的TLS實現的抽象。例如,在POSIX系統上,ThreadLocalPlatform::AllocateSlot的實現為:
void ThreadLocalPlatform::AllocateSlot(SlotType& slot) { int error = pthread_key_create(&slot, NULL); CHECK_EQ(error, 0); }
一般地,會結合LazyInstance使用ThreadLocalPointer,例如:
staticbase::LazyInstance<base::ThreadLocalPointer<RenderThread> >lazy_tls = LAZY_INSTANCE_INITIALIZER; RenderThread* RenderThread::current() { return lazy_tls.Pointer()->Get(); } RenderThread::RenderThread() { lazy_tls.Pointer()->Set(this); } RenderThread::~RenderThread() { lazy_tls.Pointer()->Set(NULL); }
當創建RenderThread實例時,當前線程會通過訪問lazy_tls設置線程的私有數據,此后每次調用current()方法時,都會返回線程私有的RenderThread實例。
由于LazyInstance是延遲初始化的,上述代碼中,當首次訪問Pointer()時,會創建一個新的ThreadLocalPointer實例,也就是向OS申請分配一個新的TLS槽。
在三大桌面操作系統上(Windows/Linux/Mac),上述對ThreadLocalPointer的封裝和實現都能工作的很好,但自從Chromium支持Android之后,潛伏在上述實現中的問題就浮出水面了。
如果一個線程中需要存儲多個私有數據,比如除了RenderThread實例,還有MessageLoop實例等等,每個都要系統分配給進程的TLS鍵值,那么一旦鍵值的數據達到上限,申請失敗導致程序崩潰。這個問題已經在Chromium WebView中暴露了,原因是Android 4.3或更老的系統,最大可用的TLS槽數量只有64個,除去Android系統庫(opengl,jvm)需要占用一部分之外,留給Chromium使用的數量已經不多了,一旦發現分配不成功,Chromium會觸發CHECK,程序立即異常退出。Android 4.4系統上,最大可用的TLS槽數量多達128個,這個問題得到一定的緩解,但仍沒有從根本上杜絕這個問題。
Chromium提供了一種新的方式解決上述問題,主要思路是Chromium自己管理TLS。
以POSIX系統為例,通過pthread_key_create創建的key對進程內所有的線程都可見,但每個線程可以綁定不同的私有數據到一個相同的key上,這就意味著整個Chromium可以共享同一個TLS槽,創建一個應用層的TLS表來徹底解決系統級別TLS槽的數量限制。
具體來說,Chromium TLS系統會首先向OS申請一個key,每個線程都將為這個key綁定一張線程私有的TLS表,Chromium設置了這張表可以容納64個表項。當線程需要一個新的TLS槽時,首先會檢查這個線程是否為key已經綁定了TLS表,如果沒有,則需要創建這樣一個TLS表并將其設置為線程私有,然后從表中按順序取一個可用項,并設置新的私有數據。不難看出,新的TLS槽并不是向OS申請的,而是向Chromium TLS申請的。
ThreadLocalStorage模板類是新的ChromiumTLS系統中對TLS的抽象,StaticSlot和Slot封裝了初始化Chromium TLS系統,創建TLS表以及設置和獲取線程私有數據的操作。因此,上述ThreadLocalPointer模板類將從直接使用ThreadLocalPlatform操作TLS,應改為使用ThreadLocalStorage::Slot, 如下:
template <typename Type> class ThreadLocalPointer { public: ThreadLocalPointer() {} ~ThreadLocalPointer() { slot_.Free(); } Type* Get() { return static_cast<Type*>(slot_.Get()); } void Set(Type* ptr) { slot_.Set(const_cast<void*>(static_cast<const void*>(ptr))); } private: ThreadLocalStorage::Slot slot_; DISALLOW_COPY_AND_ASSIGN(ThreadLocalPointer<Type>); };
再次強調,ThreadLocalStorage::Slot只是向Chromium TLS系統申請可用的TLS槽,而不是向系統直接申請。
Chromium項目的bug列表里已經報告了這個問題,參見這里。
Chromium TLS實現是最近才提交到Chromium代碼庫中的,參見這里。
Android源代碼中對TLS槽數量的定義在BIONIC_TLS_SLOTS宏常量中,見Android 4.3和Android 4.4。
關于Chromium TLS系統實現細節,感興趣的讀者可讀讀base/threading/thread_local_storage.h/cc等源文件。
關于pthread_key_create,參見Linux Man Page。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。