
关于锁的使用根据我的经验总结如下几点减少锁的使用实际开发中能不使用锁尽量不使用锁当然这不是绝对的如果使用锁也能满足性能要求使用锁也无妨一般使用了锁的代码会带来如下性能损失加锁和解锁操作本身有一定的开销临界区的代码不能并发执行进入临界区的次数过于频繁线程之间对临界区的争夺太过激烈若线程竞争互斥量失败就会陷入阻塞让出 CPU所以执行上下文切换的次数要远远多于不使用互斥量的版本。替代锁的方式有很多如无锁队列。明确锁的范围看下面这段代码if(hashtable.is_empty()) { pthread_mutex_lock(mutex); htable_insert(hashtable, elem); pthread_mutex_unlock(mutex); }读者能看出这段代码的问题吗代码行4虽然对hashtable的插入使用了锁做保护但是判断hash_table是否为空也需要使用锁保护所以正确的写法应该是pthread_mutex_lock(mutex); if(hashtable.is_empty()) { htable_insert(hashtable, elem); } pthread_mutex_unlock(mutex);减少锁的粒度所谓减小锁使用粒度指的是尽量减小锁作用的临界区代码范围临界区的代码范围越小多个线程排队进入临界区的时间就会越短。这就类似高速公路上堵车如果堵车的路段越长那么后续过来的车辆通行等待时间就会越长。我们来看两个具体的例子示例一void TaskPool::addTask(Task* task) { std::lock_guardstd::mutex guard(m_mutexList); std::shared_ptrTask spTask; spTask.reset(task); m_taskList.push_back(spTask); m_cv.notify_one(); }上述代码中guard锁保护m_taskList仔细分析下这段代码发现代码行4、5和8行其实没必要作为临界区内的代码的所以建议挪到临界区外面去修改如下void TaskPool::addTask(Task* task) { std::shared_ptrTask spTask; spTask.reset(task); { std::lock_guardstd::mutex guard(m_mutexList); m_taskList.push_back(spTask); } m_cv.notify_one(); }修改之后guard锁的作用范围就是7、8两行了仅对 **m_taskList.push_back() **操作做保护这样锁的粒度就变小了。示例二void EventLoop::doPendingFunctors() { std::unique_lockstd::mutex lock(mutex_); for (size_t i 0; i pendingFunctors_.size(); i) { pendingFunctors_[i](); } }上述代码中pendingFunctors_是被锁保护的对象它的类型是std::vectorFunctor这样的代码效率比较低必须等当前线程挨个处理完pendingFunctors_中的元素后其他线程才能操作pendingFunctors_。修改代码如下void EventLoop::doPendingFunctors() { std::vectorFunctor functors; { std::unique_lockstd::mutex lock(mutex_); functors.swap(pendingFunctors_); } for (size_t i 0; i functors.size(); i) { functors[i](); } }修改之后的代码使用了一个局部变量functors然后把pendingFunctors_中的内容倒换到functors中这样就可以释放锁了允许其他线程操作pendingFunctors_现在只要继续操作本地对象functors就可以了提高了效率。避免死锁的一些建议**一个函数中如果有一个加锁操作那么一定要记得在函数退出时记得解锁且每个退出路径上都不要忘记解锁路径。**例如void some_func() { //加锁代码 if (条件1) { //其他代码 //解锁代码 return; } else { //其他代码 //解锁代码 return; } if (条件2) { if (条件3) { //其他代码 //解锁代码 return; } if (条件4) { //其他代码 //解锁代码 return; } } while (条件5) { if (条件6) { //其他代码 //解锁代码 return; } } }上述函数中每个逻辑出口处都需要写上解锁代码。前面也说过这种逻辑非常容易因为疏忽忘记在某个地方加上解锁代码而造成死锁所以一般建议使用 RAII 技术将加锁和解锁代码封装起来线程退出时一定要及时释放其持有的锁实际开发中会因一些特殊需求创建一些临时线程这些线程执行完相应的任务后就会退出。对于这类线程如果其持有了锁一定记得在线程退出时记得释放其持有的锁对象。多线程请求锁的方向要一致以避免死锁假设现在有两个锁 A 和 B线程 1 在请求了锁 A 之后再请求 B线程 2 在请求了锁 B 后再请求锁 A这种线程请求锁的方向就不一致了线程 1 的方向是从 A 到 B线程 2 的方向是从 B 到 A多个线程请求锁的方向不一致容易造成死锁。所以建议的方式是 线程 1 和 线程 2 请求锁的方向保持一致要么都从 A 到 B要么都从 B 到 A。当需要同一个线程重复请求一个锁时搞清楚你所使用的锁的行为是递增锁引用计数还是会阻塞抑或是直接获得锁避免活锁的一些建议前面说了避免“死锁”读者应该能理解但是这里突然出现了避免“活锁”我相信很多人看到这个标题一下子就懵了。所谓活锁就是当多个线程使用trylock系列的函数时由于多个线程相互谦让导致即使在某段时间内锁资源是可用的也可能导致需要锁的线程拿不到锁。举个生活中的例子马路上两个人迎面走来两个人同时往一个方向避让原来本意是给对方让路结果还是发生了碰撞。我们在实际编码时尽量避免不要过多的线程使用trylock请求锁以免出现“活锁”现象这是对资源的一种浪费。