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