Envoy原始碼分析之Dispatcher
Dispatcher
在Envoy的程式碼中Dispatcher
是隨處可見的,可以說在Envoy中有著舉足輕重的地位,一個Dispatcher
就是一個EventLoop,其承擔了任務佇列、網路事件處理、定時器、訊號處理等核心功能。在Envoy threading model這篇文章所提到的EventLoop
(Each worker thread runs a “non-blocking” event loop
)指的就是這個Dispatcher
物件。這個部分的程式碼相對較獨立,和其他模組耦合也比較少,但重要性卻不言而喻。下面是與Dispatcher
相關的類圖,在接下來會對其中的關鍵概念進行介紹。
Dispatcher 和 Libevent
Dispatcher
本質上就是一個EventLoop,Envoy並沒有重新實現,而是複用了Libevent中的event_base
,在Libevent的基礎上進行了二次封裝並抽象出一些事件類,比如FileEvent
、SignalEvent
、Timer
等。Libevent是一個C庫,而Envoy是C++,為了避免手動管理這些C結構的記憶體,Envoy通過繼承unique_ptr
的方式重新封裝了這些libevent暴露出來的C結構。
template <class T, void (*deleter)(T*)>
class CSmartPtr : public std::unique_ptr<T, void (*)(T*)> {
public:
CSmartPtr() : std::unique_ptr<T, void (*)(T*)>(nullptr, deleter) {}
CSmartPtr(T* object) : std::unique_ptr<T, void (*)(T*)>(object, deleter) {}
};
通過CSmartPtr
就可以將Libevent中的一些C資料結構的記憶體通過RAII機制自動管理起來,使用方式如下:
extern "C" {
void event_base_free(event_base*);
}
struct evbuffer;
extern "C" {
void evbuffer_free(evbuffer*);
}
.....
typedef CSmartPtr<event_base, event_base_free> BasePtr;
typedef CSmartPtr<evbuffer, evbuffer_free> BufferPtr;
typedef CSmartPtr<bufferevent, bufferevent_free> BufferEventPtr;
typedef CSmartPtr<evconnlistener, evconnlistener_free> ListenerPtr;
在Libevent中無論是定時器到期、收到訊號、還是檔案可讀寫等都是事件,統一使用event
型別來表示,Envoy中則將event
作為ImplBase
的成員,然後讓所有的事件型別的物件都繼承ImplBase
,從而實現了事件的抽象。
class ImplBase {
protected:
~ImplBase();
event raw_event_;
};
SignalEvent
SignalEvent的實現很簡單,通過evsignal_assign
來初始化事件,然後通過evsignal_add
新增事件使事件成為未決狀態(關於Libevent事件狀態見附錄)。
class SignalEventImpl : public SignalEvent, ImplBase {
public:
// signal_num: 要設定的訊號值
// cb: 訊號事件的處理函式
SignalEventImpl(DispatcherImpl& dispatcher, int signal_num, SignalCb cb);
private:
SignalCb cb_;
};
SignalEventImpl::SignalEventImpl(DispatcherImpl& dispatcher,
int signal_num, SignalCb cb) : cb_(cb) {
evsignal_assign(
&raw_event_, &dispatcher.base(), signal_num,
[](evutil_socket_t, short, void* arg) -> void {
static_cast<SignalEventImpl*>(arg)->cb_(); },
this);
evsignal_add(&raw_event_, nullptr);
}
Timer
Timer事件暴露了兩個介面一個用於關閉Timer,另外一個則用於啟動Timer,需要傳遞一個時間來設定Timer的到期時間間隔。
class Timer {
public:
virtual ~Timer() {}
virtual void disableTimer() PURE;
virtual void enableTimer(const std::chrono::milliseconds& d) PURE;
};
建立Timer的時候會通過evtimer_assgin
對event進行初始化,這個時候事件還處於未決狀態而不會觸發,需要通過event_add
新增到Dispatcher
中才能被觸發。
class TimerImpl : public Timer, ImplBase {
public:
TimerImpl(Libevent::BasePtr& libevent, TimerCb cb);
// Timer
void disableTimer() override;
void enableTimer(const std::chrono::milliseconds& d) override;
private:
TimerCb cb_;
};
TimerImpl::TimerImpl(DispatcherImpl& dispatcher, TimerCb cb) : cb_(cb) {
ASSERT(cb_);
evtimer_assign(
&raw_event_, &dispatcher.base(),
[](evutil_socket_t, short, void* arg) -> void {
static_cast<TimerImpl*>(arg)->cb_(); },
this);
}
disableTimer
被呼叫時其內部會呼叫event_del
來刪除事件,使事件成為非未決狀態,enableTimer
被呼叫時則間接呼叫event_add
使事件成為未決狀態,這樣一旦超時時間到了就會觸發超時事件。
void TimerImpl::disableTimer() { event_del(&raw_event_); }
void TimerImpl::enableTimer(const std::chrono::milliseconds& d) {
if (d.count() == 0) {
event_active(&raw_event_, EV_TIMEOUT, 0);
} else {
std::chrono::microseconds us =
std::chrono::duration_cast<std::chrono::microseconds>(d);
timeval tv;
tv.tv_sec = us.count() / 1000000;
tv.tv_usec = us.count() % 1000000;
event_add(&raw_event_, &tv);
}
}
上面的程式碼在計算
timer
時間timeval
的時候實現的並不優雅,應該避免使用像1000000
這樣的不具備可讀性的數字常量,社群中有人建議可以改成如下的形式。
auto secs = std::chrono::duration_cast<std::chrono::seconds>(d);
auto usecs =
std::chrono::duration_cast<std::chrono::microseconds>(d - secs);
tv.tv_secs = secs.count();
tv.tv_usecs = usecs.count();
FileEvent
socket
套接字相關的事件被封裝為FileEvent
,其上暴露了二個介面:activate
用於主動觸發事件,典型的使用場景比如: 喚醒EventLoop、Write Buffer有資料,可以主動觸發下可寫事件(Envoy中的典型使用場景)等;setEnabled
用於設定事件型別,將事件新增到EventLoop
中使其成為未決狀態。
void FileEventImpl::activate(uint32_t events) {
int libevent_events = 0;
if (events & FileReadyType::Read) {
libevent_events |= EV_READ;
}
if (events & FileReadyType::Write) {
libevent_events |= EV_WRITE;
}
if (events & FileReadyType::Closed) {
libevent_events |= EV_CLOSED;
}
ASSERT(libevent_events);
event_active(&raw_event_, libevent_events, 0);
}
void FileEventImpl::setEnabled(uint32_t events) {
event_del(&raw_event_);
assignEvents(events);
event_add(&raw_event_, nullptr);
}
任務佇列
Dispatcher
的內部有一個任務佇列,也會建立一個執行緒專們處理任務佇列中的任務。通過Dispatcher
的post
方法可以將任務投遞到任務佇列中,交給Dispatcher
內的執行緒去處理。
void DispatcherImpl::post(std::function<void()> callback) {
bool do_post;
{
Thread::LockGuard lock(post_lock_);
do_post = post_callbacks_.empty();
post_callbacks_.push_back(callback);
}
if (do_post) {
post_timer_->enableTimer(std::chrono::milliseconds(0));
}
}
post
方法將傳遞進來的callback
所代表的任務,新增到post_callbacks_
所代表的型別為vector<callback>
的成員表變數中。如果post_callbacks_
為空的話,說明背後的處理執行緒是處於非活動狀態,這時通過post_timer_
設定一個超時時間時間為0的方式來喚醒它。post_timer_
在構造的時候就已經設定好對應的callback
為runPostCallbacks
,對應程式碼如下:
DispatcherImpl::DispatcherImpl(TimeSystem& time_system,
Buffer::WatermarkFactoryPtr&& factory)
: ......
post_timer_(createTimer([this]() -> void { runPostCallbacks(); })),
current_to_delete_(&to_delete_1_) {
RELEASE_ASSERT(Libevent::Global::initialized(), "");
}
runPostCallbacks
是一個while迴圈,每次都從post_callbacks_
中取出一個callback
所代表的任務去執行,直到post_callbacks_
為空。每次執行runPostCallbacks
都會確保所有的任務都執行完。顯然,在runPostCallbacks
被執行緒執行的期間如果post
進來了新的任務,那麼新任務直接追加到post_callbacks_
尾部即可,而無需做喚醒執行緒這一動作。
void DispatcherImpl::runPostCallbacks() {
while (true) {
std::function<void()> callback;
{
Thread::LockGuard lock(post_lock_);
if (post_callbacks_.empty()) {
return;
}
callback = post_callbacks_.front();
post_callbacks_.pop_front();
}
callback();
}
}
DeferredDeletable
最後講一下Dispatcher
中比較難理解也很重要的DeferredDeletable
,它是一個空介面,所有要進行延遲析構的物件都要繼承自這個空介面。在Envoy的程式碼中像下面這樣繼承自DeferredDeletable
的類隨處可見。
class DeferredDeletable {
public:
virtual ~DeferredDeletable() {}
};
那何為延遲析構呢?用在哪個場景呢?延遲析構指的是將析構的動作交由Dispatcher
來完成,所以DeferredDeletable
和Dispatcher
密切相關。Dispatcher
物件有一個vector
儲存了所有要延遲析構的物件。
class DispatcherImpl : public Dispatcher {
......
private:
........
std::vector<DeferredDeletablePtr> to_delete_1_;
std::vector<DeferredDeletablePtr> to_delete_2_;
std::vector<DeferredDeletablePtr>* current_to_delete_;
}
to_delete_1_
和to_delete_2_
就是用來存放所有的要延遲析構的物件,這裡使用兩個vector
存放,為什麼要這樣做呢?。current_to_delete_
始終指向當前正要析構的物件列表,每次執行完析構後就交替指向另外一個物件列表,來回交替。
void DispatcherImpl::clearDeferredDeleteList() {
ASSERT(isThreadSafe());
std::vector<DeferredDeletablePtr>* to_delete = current_to_delete_;
size_t num_to_delete = to_delete->size();
if (deferred_deleting_ || !num_to_delete) {
return;
}
ENVOY_LOG(trace, "clearing deferred deletion list (size={})", num_to_delete);
if (current_to_delete_ == &to_delete_1_) {
current_to_delete_ = &to_delete_2_;
} else {
current_to_delete_ = &to_delete_1_;
}
deferred_deleting_ = true;
for (size_t i = 0; i < num_to_delete; i++) {
(*to_delete)[i].reset();
}
to_delete->clear();
deferred_deleting_ = false;
}
上面的程式碼在執行物件析構的時候先使用to_delete
來指向當前正要析構的物件列表,然後將current_to_delete_
指向另外一個列表,這樣在新增延遲刪除的物件時,就可以做到安全的把物件新增到列表中了。因為deferredDelete
和clearDeferredDeleteList
都是在同一個執行緒中執行,所以current_to_delete_
是一個普通的指標,可以安全的更改指標指向另外一個,而不用擔心有執行緒安全問題。
void DispatcherImpl::deferredDelete(DeferredDeletablePtr&& to_delete) {
ASSERT(isThreadSafe());
current_to_delete_->emplace_back(std::move(to_delete));
ENVOY_LOG(trace, "item added to deferred deletion list (size={})", current_to_delete_->size());
if (1 == current_to_delete_->size()) {
deferred_delete_timer_->enableTimer(std::chrono::milliseconds(0));
}
}
當有要進行延遲析構的物件時,呼叫deferredDelete
即可,這個函式內部會通過current_to_delete_
把物件放到要延遲析構的列表中,最後判斷下當前要延遲析構的列表大小是否是1,如果是1表明這是第一次新增延遲析構的物件,那麼就需要通過deferred_delete_timer_
把背後的執行緒喚醒執行clearDeferredDeleteList
函式。這樣做的原因是避免多次喚醒,因為有一種情況是執行緒已經喚醒了正在執行clearDeferredDeleteList
,在這個過程中又有其他的物件需要析構而加入到vector
中。
到此為止deferredDelete
的實現原理就基本分析完了,可以看出它的實現和任務佇列的實現很類似,只不過一個是迴圈執行callback
所代表的任務,另一個是對物件進行析構。最後我們來看一下deferredDelete
的應用場景,卻“為何要進行延遲析構?”在Envoy的原始碼中經常會看到像下面這樣的程式碼片段。
ConnectionImpl::ConnectionImpl(Event::Dispatcher& dispatcher,
ConnectionSocketPtr&& socket,
TransportSocketPtr&& transport_socket,
bool connected) {
......
}
// 傳遞裸指標到回撥中
file_event_ = dispatcher_.createFileEvent(
fd(), [this](uint32_t events) -> void { onFileEvent(events); },
Event::FileTriggerType::Edge,
Event::FileReadyType::Read | Event::FileReadyType::Write);
......
}
傳遞給Dispatcher
的callback
都是通過裸指標的方式進行回撥,如果進行回撥的時候物件已經析構了,就會出現野指標的問題,我相信C++水平還可以的同學都會看出這個問題,除非能在邏輯上保證Dispatcher
的生命週期比所有物件都短,這樣就能保證在回撥的時候物件肯定不會析構,但是這不可能成立的,因為Dispatcher
是EventLoop
的核心。
一個執行緒執行一個EventLoop
直到執行緒結束,Dispatcher
物件才會析構,這意味著Dispatcher
物件的生命週期是最長的。所以從邏輯上沒辦法保證進行回撥的時候物件沒有析構。可能有人會有疑問,物件在析構的時候把註冊的事件取消不就可以避免野指標的問題嗎? 那如果事件已經觸發了,callback
正在等待執行呢? 又或者callback
執行了一半呢?前者libevent是可以保證的,在呼叫event_del
的時候可以把處於等待執行的事件取消掉,但是後者就無能為力了,這個時候如果物件析構了,那行為就是未定義了。沿著這個思路想一想,是不是隻要保證物件析構的時候沒有callback
正在執行就可以解決問題了呢?是的,只要保證所有在執行中的callback
執行完了,再做物件析構就可以了。可以利用Dispatcher
是順序執行所有callback
的特點,向Dispatcher
中插入一個任務就是用來物件析構的,那麼當這個任務執行的時候是可以保證沒有其他任何callback
在執行。通過這個方法就完美解決了這裡遇到的野指標問題了。
或許有人又會想,這裡是不是可以用shared_ptr和shared_from_this來解這個呢? 是的,這是解決多執行緒環境下物件析構的祕密武器,通過延長物件的生命週期,把物件的生命週期延長到和callback
一樣,等callback
執行完再進行析構,同樣可以達到效果,但是這帶來了兩個問題,第一就是物件生命週期被無限拉長,雖然延遲析構也拉長了生命週期,但是時間是可預期的,一旦EventLoop
執行了clearDeferredDeleteList
任務就會立刻被回收,而通過shared_ptr
的方式其生命週期取決於callback
何時執行,而callback
何時執行這個是沒辦法保證的,比如一個等待socket
的可讀事件進行回撥,如果對端一直不傳送資料,那麼callback
就一直不會被執行,物件就一直無法被析構,長時間累積會導致記憶體使用率上漲。第二就是在使用方式上侵入性較強,需要強制使用shared_ptr
的方式建立物件。
總結
Dispatcher
總的來說其實現還是比較簡單明瞭的,比較容易驗證其正確性,同樣功能也相對較弱,和chromium的MessageLoop
、boost的asio
都是相似的用途,但是功能上差得比較多。好在這是專門給Envoy設計的,而且Envoy的場景也比較單一,不必做成那麼通用的。另外一個我覺得比較奇怪的是,為什麼在DeferredDeletable
的實現中要用to_delete_1_
和to_delete_2_
兩個佇列交替來存放,其實按照我的理解一個佇列即可,因為clearDeferredDeleteList
和deferredDelete
是保證在同一個執行緒中執行的,就和Dispatcher
的任務佇列一樣,用一個佇列儲存所有要執行的任務,迴圈的執行即可。但是Envoy中沒有這樣做,我理解這樣設計的原因可能是因為相比於任務佇列來說延遲析構的重要性更低一些,大量物件的析構如果儲存在一個佇列中迴圈的進行析構勢必會影響其他關鍵任務的執行,所以這裡拆分成兩個佇列,多個任務交替的執行,就好比把一個大任務拆分成了好幾個小任務順序來執行。
附錄
- Libevent狀態轉換圖
相關文章
- Envoy 原始碼分析--LDS原始碼
- OkHttpClient原始碼分析(一)—— 同步、非同步請求分析和Dispatcher的任務排程HTTPclient原始碼非同步
- Guava 原始碼分析之 EventBus 原始碼分析Guava原始碼
- Android 原始碼分析之 AsyncTask 原始碼分析Android原始碼
- 原始碼分析之 HashMap原始碼HashMap
- 原始碼分析之 LinkedList原始碼
- 原始碼|jdk原始碼之HashMap分析(一)原始碼JDKHashMap
- 原始碼|jdk原始碼之HashMap分析(二)原始碼JDKHashMap
- 死磕 jdk原始碼之HashMap原始碼分析JDK原始碼HashMap
- Android 原始碼分析之 EventBus 的原始碼解析Android原始碼
- lodash原始碼分析之isArguments原始碼
- Fresco原始碼分析之DraweeView原始碼View
- Netty原始碼分析之LengthFieldBasedFrameDecoderNetty原始碼
- RecyclerView之SnapHelper原始碼分析View原始碼
- tornado 原始碼之 coroutine 分析原始碼
- lodash原始碼分析之isObjectLike原始碼Object
- OpenGL 之 GPUImage 原始碼分析GPUUI原始碼
- jdk原始碼分析之TreeMapJDK原始碼
- 原始碼分析Kafka之Producer原始碼Kafka
- DRF之Response原始碼分析原始碼
- Spring AOP之原始碼分析Spring原始碼
- BlueStore原始碼分析之FreelistManager原始碼
- JUC之ReentrantLock原始碼分析ReentrantLock原始碼
- JUC之CountDownLatch原始碼分析CountDownLatch原始碼
- Fresco原始碼分析之Hierarchy原始碼
- Dubbo之SPI原始碼分析原始碼
- MongoDB原始碼分析之MongosXFMongoDB原始碼
- Flutter原始碼分析之InheritedWidgetFlutter原始碼
- Redux原始碼分析之createStoreRedux原始碼
- Java原始碼分析:Guava之不可變集合ImmutableMap的原始碼分析Java原始碼Guava
- Spring原始碼分析——spring原始碼之obtainFreshBeanFactory()介紹Spring原始碼AIBean
- spring原始碼分析之freemarker整合Spring原始碼
- netty原始碼分析之pipeline(二)Netty原始碼
- Spring原始碼分析之IoC(一)Spring原始碼
- 原始碼分析 @angular/cdk 之 Portal原始碼Angular
- netty原始碼分析之pipeline(一)Netty原始碼
- Spring原始碼分析之IoC(二)Spring原始碼
- SpringMVC之原始碼分析--ViewResolver(五)SpringMVC原始碼View