Ceph 擼源碼系列(二):Ceph源代碼裏的那些鎖 std::mutex(2 of 3)

一、前言:

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> 的共存。它們用於各自的場景。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章