一、前言:
Nautilus v14.2.4 裏有一個Performance PR msg/async: avoid put message within write_lock #20731 ,這個PR主要是把 for 循環裏的m->put()的代碼放到鎖之外,來減少臨界區裏的代碼,以提高performance。
注:這個PR裏由於要把put()代碼分開,新增了Message *數組,用來指向std::list 類型的sent消息。所以新版本里把m->put()替換成了pending[k]->put()
二、分析這個PR
1. 先過一下這個PR的diff 代碼
2. 先分析上面PR的關鍵代碼:
- 刪除了 std::lock_guard<std::mutex> l(write_lock)
- 增加了write_lock.lock(); write_lock.unlock();
到提問題的環節了,什麼是 std::lock_guard?write_lock變量定義在哪?
1)什麼是 std::lock_guard?
互斥類的最重要成員函數是lock()和unlock()。在進入臨界區時,執行lock()加鎖操作,如果這時已經被其它線程鎖住,則當前線程在此排隊等待。退出臨界區時,執行unlock()解鎖操作。
更好的辦法是採用”資源分配時初始化”(RAII)方法來加鎖、解鎖,這避免了在臨界區中因爲拋出異常或return等操作導致沒有解鎖就退出的問題。它極大地簡化了程序員編寫mutex相關的異常處理代碼。C++11的標準庫中提供了std::lock_guard類模板做mutex的RAII。
std::lock_guard對象並不負責管理mutex對象的生命週期,lock_guard對象只是簡化了mutex對象的上鎖和解鎖操作,方便線程對互斥量上鎖,即在某個lock_guard對象的生命週期內,它所管理的鎖對象會一直保持上鎖狀態;而lock_guard的生命週期結束之後,它所管理的鎖對象會被解鎖。程序員可以非常方便地使用lock_guard,而不用擔心異常安全問題。
小結:在std::lock_guard對象構造時,傳入的mutex對象(即它所管理的mutex對象)會被當前線程鎖住。在lock_guard對象被析構時,它所管理的mutex對象會自動解鎖,不需要程序員手動調用lock和unlock對mutex進行上鎖和解鎖操作。
詳細介紹參考:C++鎖的管理-- std::lock_guard和std::unique_lock
2)write_lock變量定義在哪
write_lock其實就是AsyncConnection類裏的一個 std::mutex類型 成員變量
//AsyncConnection.h文件裏
/*
* AsyncConnection maintains a logic session between two endpoints. In other
* word, a pair of addresses can find the only AsyncConnection. AsyncConnection
* will handle with network fault or read/write transactions. If one file
* descriptor broken, AsyncConnection will maintain the message queue and
* sequence, try to reconnect peer endpoint.
*/
class AsyncConnection : public Connection {
...
std::mutex write_lock;
...
}
3. 分析整個PR
1) PR版本前的代碼
void AsyncConnection::handle_ack(uint64_t seq)
{
ldout(async_msgr->cct, 15) << __func__ << " got ack seq " << seq << dendl;
// trim sent list
std::lock_guard<std::mutex> l(write_lock);
while (!sent.empty() && sent.front()->get_seq() <= seq) {
Message* m = sent.front();
sent.pop_front();
ldout(async_msgr->cct, 10) << __func__ << " got ack seq "
<< seq << " >= " << m->get_seq() << " on "
<< m << " " << *m << dendl;
m->put();
}
}
由於一進入AsyncConnection::handle_ack()函數,就定義了 std::lock_guard<std::mutex> l(write_lock),std::lock_guard構造函數裏就調用了write_lock.lock加鎖,一直到函數結束才釋放鎖。
2) PR版本後的代碼:
void AsyncConnection::handle_ack(uint64_t seq)
{
ldout(async_msgr->cct, 15) << __func__ << " got ack seq " << seq << dendl;
// trim sent list
static const int max_pending = 128;
int i = 0;
Message *pending[max_pending];
write_lock.lock();
while (!sent.empty() && sent.front()->get_seq() <= seq && i < max_pending) {
Message* m = sent.front();
sent.pop_front();
pending[i++] = m;
ldout(async_msgr->cct, 10) << __func__ << " got ack seq "
<< seq << " >= " << m->get_seq() << " on "
<< m << " " << *m << dendl;
}
write_lock.unlock();
for (int k = 0; k < i; k++)
pending[k]->put();
}
去掉了std::lock_guard<std::mutex> l(write_lock)代碼,使用了純手工write_lock.lock()/write_lock.unlock()來指定臨界區區間。把之前的m->put()放到了臨界區之外,以此來提高performance。這個屬於代碼級別的性能優化。
三、總結
std::lock_guard<std::mutex> l(write_lock):由於鎖lock的生命週期在整個std::lock_guard<std::mutex>變量裏有效,所以它的生命週期控制精準度相比write_lock.lock()/write_lock.unlock()低。但std::lock_guard<std::mutex>避免了在臨界區中因爲拋出異常或return等操作導致沒有解鎖就退出的問題,這是其優點。
write_lock.lock()/write_lock.unlock():其鎖lock的生命週期控制精準度相比std::lock_guard<std::mutex>高,所以在CEPH源代碼裏看到了write_lock.lock()/write_lock.unlock() 和 std::lock_guard<std::mutex> 的共存。它們用於各自的場景。