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> 的共存。它们用于各自的场景。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值