天天看點

Envoy源碼分析之ThreadLocal機制ThreadLocal機制ThreadLocalObjectThreadLocal小結

ThreadLocal機制

​ Envoy中的

ThreadLocal

機制其實就是我們經常說的線程本地存儲簡稱TLS(Thread Local Storage),顧名思義通過TLS定義的變量會在每一個線程專有的存儲區域存儲一份,通路TLS的時候,其實通路的是目前線程占有存儲區域中的副本,是以可以使得線程可以無鎖的并發通路同一個變量。Linux上一般有三種方式來定義一個TLS變量。

  • gcc對C語言的擴充

    __thread

  • pthread庫提供的

    pthread_key_create

  • C++11的

    std::thread_local

    關鍵字

​ Envoy的

ThreadLocal

機制就是在C++11的

std::thread_local

基礎上進行了封裝用于實作線程間的資料共享。Envoy因其配置的動态生效而出名,而配置動态生效的基石就是

ThreadLocal

機制,通過

ThreadLocal

機制将配置可以無鎖的在多個線程之間共享,當配置發生變更的時候,通過主線程将更新後的配置Post到各個線程中,交由各個線程來更新自己的

ThreadLocal

ThreadLocalObject

​ Envoy要求所有的

ThreadLocal

資料對象都要繼承

ThreadLocalObject

,比如下面這個

ThreadLocal

對象。

struct ThreadLocalCachedDate : public ThreadLocal::ThreadLocalObject {
   ThreadLocalCachedDate(const std::string& date_string) : 
   date_string_(date_string) {}
  const std::string date_string_;
};           

​ 但實際上

ThreadLocalObject

隻是一個空的接口類,是以并非我們繼承了

ThreadLocalObject

就是一個TLS了。繼承

ThreadLocalObject

目的是為了可以統一對所有要進行TLS的對象進行管理。

class ThreadLocalObject {
public:
  virtual ~ThreadLocalObject() = default;
};
using ThreadLocalObjectSharedPtr = std::shared_ptr<ThreadLocalObject>;           

​ Envoy中需要TLS的資料有很多,最重要的當屬配置,随着配置的增多,這類資料所占據的記憶體也會變得很大,如果每一種配置都聲明為TLS會導緻不少記憶體浪費。為此Envoy通過

ThreadLocalData

将所有要進行TLS的對象都管理起來,然後将

ThreadLocalData

本身設定為TLS,通過TLS中儲存的指針來通路對應的資料。這樣就可以避免直接在TLS中儲存資料而帶來記憶體上的浪費,隻需要儲存指向資料的指針即可,相關代碼如下。

struct ThreadLocalData {
  // 指向目前線程的Dispatcher對象 
  Event::Dispatcher* dispatcher_{};
  // 儲存了所有要TLS的資料對象的智能指針,通過智能指針來通路真正的資料對象
  std::vector<ThreadLocalObjectSharedPtr> data_;
};           
Envoy源碼分析之ThreadLocal機制ThreadLocal機制ThreadLocalObjectThreadLocal小結
如上圖所示,每一個TLS通過指針指向實際的對象,每一個資料對象隻在記憶體中儲存一份,避免記憶體上的浪費,但是這樣帶來問題就是如何做到線程安全的通路資料對象呢?  當我們要通路資料對象的時候,如果此時正在對資料對象進行更新,這個時候就會存在一個線程安全的問題了。Envoy巧妙的通過在資料對象更新的時候,先構造出一個新的資料對象,然後将TLS中的資料對象指針指向新的資料對象來實作線程安全的通路。本質上和COW(copy-on-write)很類似,但是存在兩點差別。
           
  • COW中是先拷貝原來的對象,然後更改對象,而Envoy在這裡是重新建構一個新的資料對象
  • COW中無論是讀還是寫,在更改

    shared_ptr

    指向時,都需要加鎖,因為

    shared_ptr

    本身的讀寫時非線程安全的,而Envoy不需要加鎖。

​ Envoy中指向資料對象的

shared_ptr

并非隻有一個,而是每一個線程都有一個

shared_ptr

指向資料對象,更改

shared_ptr

指向新的資料對象時通過post一個任務到對應線程中,然後在同一個線程使

shared_ptr

指向新的資料對象,是以并沒有多線程操作

shared_ptr

,是以沒有線程安全問題,自然也不用加鎖,這是Envoy實作比較巧妙的地方。

Envoy源碼分析之ThreadLocal機制ThreadLocal機制ThreadLocalObjectThreadLocal小結

​ 如上圖所示,T1時刻,Thread1通過TLS對象通路

ThreadLocalObjectOld

,在T2時刻在main線程發現配置發生了變化,重新構造了一個新的

ThreadlocalObjectNew

對象,然後通過Thread1的

Dispatcher

對象post了一個任務到Thread1線程,到了T3時刻這個任務開始執行,将對應的指針指向了

ThreadLocalObjectNew

,最後在T4時刻再次通路配置的時候,就已經通路的是最新的配置了。到此為止就完成了一次配置更新,而且整個過程是線程安全的。

ThreadLocal

​ 終于到了分析真正的ThreadLocal對象的時候,它的功能其實很簡單,大部分的能力都是依賴

Dispatcher

、還有上文中提到的

SlotImpl

ThreadLocalData

等,

Instance

是它的接口類,它繼承了

SlotAllocator

接口,也包含了上文中分析的

allocateSlot

方法。

class Instance : public SlotAllocator {
public:
  // 每啟動一個worker線程就需要通過這個方法進行注冊
  virtual void registerThread(Event::Dispatcher& dispatcher, bool main_thread) PURE;
  // 主線程在退出的時候調用,用于标記shutdown狀态
  virtual void shutdownGlobalThreading() PURE;
  // 每一個worker線程需要調用這個方法來釋放自己的TLS
  virtual void shutdownThread() PURE;
  virtual Event::Dispatcher& dispatcher() PURE;
};           

​ 對應的實作是

InstanceImpl

對象,在

Instance

的基礎上又擴充了一些post任務到所有線程的一些方法。

class InstanceImpl : public Instance {
 public:
    ....
 private:
  // post任務到所有注冊的線程中
  void runOnAllThreads(Event::PostCb cb);
  // post任務到所有注冊的線程中,完成後通過main_callback進行通知
  void runOnAllThreads(Event::PostCb cb, Event::PostCb main_callback);
  // 初始化TLS指向對應的資料對象指針
  static void setThreadLocal(uint32_t index, ThreadLocalObjectSharedPtr object);
  .....
  // 儲存所有注冊的線程
  std::list<std::reference_wrapper<Event::Dispatcher>> registered_threads_;           

​ 因為所有的線程都會注冊都

InstanceImpl

中,是以隻需要周遊所有的線程所對應的

Dispatcher

對象,調用其post方法将任務投遞到對應線程即可,但是如何做到等所有任務執行完成後進行通知呢 ?

void InstanceImpl::runOnAllThreads(Event::PostCb cb, 
                                   Event::PostCb all_threads_complete_cb) {
  ASSERT(std::this_thread::get_id() == main_thread_id_);
  ASSERT(!shutdown_);
  // 首先在主線程執行任務
  cb();
  // 利用了shared_ptr自定義析構函數,在析構的時候向主線程post一個完成的通知任務
  // 這個機制和Bookkeeper的實作機制是一樣的。
  std::shared_ptr<Event::PostCb> cb_guard(new Event::PostCb(cb),
                   [this, all_threads_complete_cb](Event::PostCb* cb) {
                    main_thread_dispatcher_->post(all_threads_complete_cb);
                      delete cb; });

  for (Event::Dispatcher& dispatcher : registered_threads_) {
    dispatcher.post([cb_guard]() -> void { (*cb_guard)(); });
  }
}           

​ 通過上面的代碼可以看到,這裡仍然利用到了

shared_ptr

的引用計數機制來實作的。每一個post到其他線程的任務都會導緻

cb_guard

引用計數加1,post任務執行完成後

cb_guard

引用計數減1,等全部任務完成後,

cb_guard

的引用計數就變成0了,這個時候就會執行自定義的删除器,在删除器中就會post一個任務到主線程中,進而實作了任務執行完成的通知回調機制。

​ 接下來我們來分析下

shutdownGlobalThreading

,這個函數是用于設定flag來表示正在關閉TLS,必須由主線程在其它worker線程退出之前來調用,調用完成後每一個worker線程還需要調用對應TLS的

shutdownThread

來清理TLS中的對象,到此為止才完成了全部的TLS清理工作。

void InstanceImpl::shutdownGlobalThreading() {
  ASSERT(std::this_thread::get_id() == main_thread_id_);
  ASSERT(!shutdown_);
  shutdown_ = true;
}           

上面的代碼是

shutdownGlobalThreading

的實作,可以看到僅僅是設定了一個

shutdown_

的标志。

​ 最後來分析一下

shutdownThread

,每一個work線程在退出事都需要調用這個函數,這個函數會将存儲的所有線程存儲的對象進行清除。每一個worker線程都持有

InstanceImpl

執行個體的引用,在析構的時候會調用

shutdownThread

來釋放自己線程的TLS内容,這個函數的實作如下:

void InstanceImpl::shutdownThread() {
  ASSERT(shutdown_);
  for (auto it = thread_local_data_.data_.rbegin(); 
       it != thread_local_data_.data_.rend(); ++it) {
    it->reset();
  }
  thread_local_data_.data_.clear();
}           

​ 比較奇怪的點在于這裡是逆序周遊所有的

ThreadLocalObject

對象來進行reset的,這是因為一些"持久"(活的比較長)的對象如

ClusterManagerImpl

很早就會建立

ThreadLocalObject

對象,但是直到shutdown的時候也不析構,而在此基礎上依賴

ClusterManagerImpl

的對象的如

GrpcClientImpl

等,則是後建立

ThreadLocalObject

對象,如果

ClusterManagerImpl

建立的

ThreadLocalObject

對象先析構,而

GrpcClientImpl

相關的

ThreadLocalObject

對象依賴了

ClusterManagerImpl

相關的TLS内容,那麼後析構就會導緻未定義的問題。為此這裡選擇逆序來進行

reset

,先從一個高層的對象開始,最後才開始對一些基礎的對象所關聯的

ThreadLocalObject

進行

reset

。例如下面這個例子:

struct ThreadLocalPool : public ThreadLocal::ThreadLocalObject {
    .....
  InstanceImpl& parent_;
  Event::Dispatcher& dispatcher_;
  Upstream::ThreadLocalCluster* cluster_;
    .....
};           

redis_proxy

中定義了一個

ThreadLocalPool

,這個

ThreadLocalPool

又依賴較為基礎的

ThreadLocalCluster

(是

ThreadLocalClusterManagerImpl

的資料成員,也就是

ClusterManagerImpl

所對應的

ThreadLocalObject

對象),如果

shutdownThread

按照順序的方式析構的話,那麼

ThreadLocalPool

中使用的

ThreadLocalCluster

會先被析構,然後才是

ThreadLocalPool

的析構,而

ThreadLocalPool

析構的時候又會使用到

ThreadLocalCluster

,但是

ThreadLocalCluster

已經析構了,這個時候就會出現野指針的問題了。

ThreadLocalPool::ThreadLocalPool(InstanceImpl& parent, 
                                 Event::Dispatcher& dispatcher, const 
                                 std::string& cluster_name)
    : parent_(parent), dispatcher_(dispatcher), 
    cluster_(parent_.cm_.get(cluster_name)) {
  .....
  local_host_set_member_update_cb_handle_ = 
  cluster_->prioritySet().addMemberUpdateCb(
      [this](uint32_t, const std::vector<Upstream::HostSharedPtr>&,
             const std::vector<Upstream::HostSharedPtr>& hosts_removed) -> void {
        onHostsRemoved(hosts_removed);
      });
}

ThreadLocalPool::~ThreadLocalPool() {
  // local_host_set_member_update_cb_handle_是ThreadLocalCluster的一部分
  // ThreadLocalCluster析構會導緻local_host_set_member_update_cb_handle_變成野指針
  local_host_set_member_update_cb_handle_->remove();
  while (!client_map_.empty()) {
    client_map_.begin()->second->redis_client_->close();
  }
}           

​ 到此為止關于Envoy中的TLS實作就全部分析完畢了。

小結

​ 通過本節的分析相信我們應該足以駕馭Envoy中的

ThreadLocal

,從其設計可以看出它的一些其巧妙之處,比如抽象出一個

Slot

和對應的線程存儲進行了關聯,

Slot

可以任意傳遞,因為不包含實際的資料,拷貝的開銷很低,隻包含了一個索引值,具體關聯的線程存儲資料是不知道的,避免直接暴露給使用者背後的資料。而

InstanceImpl

對象則管理着所有

Slot

的配置設定和移除以及整個

ThreadLocal

對象的

shutdown

。還有引入的Bookkeeper機制也甚是巧妙,和

Envoy源碼分析之Dispatcher機制

一文中的

DeferredDeletable

機制有着異曲同工之妙,通過這個機制可以做到安全的析構

SlotImpl

對象

繼續閱讀