C++雾中风景12:聊聊C++中的Mutex,以及拯救生产力的Boost

笔者近期在工做之中编程实现一个Cache结构的封装,须要使用到C++之中的互斥量Mutex,因而花了一些时间进行了调研。(结果对C++标准库非常绝望....)最终仍是经过利用了Boost库的shared_mutex解决了问题。借这个机会来聊聊在C++之中的多线程编程的一些“坑”面试

1.C++多线程编程的困扰

C++从11开始在标准库之中引入了线程库来进行多线程编程,在以前的版本须要依托操做系统自己提供的线程库来进行多线程的编程。(其实自己就是在标准库之上对底层的操做系统多线程API统一进行了封装,笔者本科时进行操做系统实验是就是使用的pthread或<windows.h>来进行多线程编程的
提供了统一的多线程当然是好事,可是标准库给的支持实在是有限,具体实践起来仍是让人挺困扰的:编程

  • C++自己的STL并非线程安全的。因此缺乏了相似与Java并发库所提供的一些高性能的线程安全的数据结构。(Doug Lea大神亲自操刀完成的并发编程库,让JDK5成为Java之中里程碑式的版本)
  • 若是没有线程安全的数据结构,退而求其次,能够本身利用互斥量Mutex来实现。C++的标准库支持以下的互斥量的实现:
互斥量 版本 做用
mutex C++11 最基本的互斥量
timed_mutex C++11 有超时机制的互斥量
recursive_mutex C++11 可重入的互斥量
recursive_timed_mutex C++11 结合 2,3 特色的互斥量
shared_timed_mutex C++14 具备超时机制的可共享互斥量
shared_mutex C++17 共享的互斥量

由上述表格可见,C++是从14以后的版本才正式支持共享互斥量,也就是实现读写锁的结构。因为笔者的公司仅支持C++11的版本,因此就没有办法使用共享互斥量来实现读写锁了。因此最终笔者只好求助与boost的库,利用boost提供的读写锁来完成了所需完成的工做。(因此对工具不足时能够考虑求助于boost库,确实是解放生产力的大杀器,C++的标准库实在太简陋了~~)windows

2.标准库互斥量的剖析

虽然吐槽了一小节,但并不影响继续去学习C++标准库给咱们提供的工具.........(希望公司能再推进升级一波C++的版本~~不过看起来是遥遥无期了)接下来笔者就要来带领你们简单剖析一些C++标准库之中互斥量。安全

mutex

mutex的中文翻译就是互斥量,不少人喜欢称之其为锁。其实不是太准确,由于多线程编程本质上应该经过互斥量之上加锁,解锁的操做,来实现多线程并发执行时对互斥资源线程安全的访问。 咱们来看看mutex类的使用方法:数据结构

long num = 0;
std::mutex num_mutex;

void numplus() {
    num_mutex.lock();
    for (long i = 0; i < 1000000; ++i) {
        num++;
    }
    num_mutex.unlock();
};

void numsub() {
    num_mutex.lock();
    for (long i = 0; i < 1000000; ++i) {
        num--;
    }
    num_mutex.unlock();
}

int main() {
    std::thread t1(numplus);
    std::thread t2(numsub);
    t1.join();
    t2.join();
    std::cout << num << std::endl;
}

调用线程从成功调用lock()或try_lock()开始,到unlock()为止占有mutex对象。当存在某线程占有mutex时,全部其余线程若调用lock则会阻塞,而调用try_lockh会获得false返回值。由上述代码能够看到,经过mutex加锁的方式,来确保只有单一线程对临界区的资源进行操做。
time_mutex与recursive_mutex的使用也是大同小异,二者都是基于mutex来实现的。( 本质上是基于recursive_mutex实现的,mutex为recursive_mutex的特例)
time_mutex则是进行加锁时能够设置阻塞的时间,若超过对应时长,则返回false。
recursive_mutex则让单一线程能够屡次对同一互斥量加锁,一样,解锁时也须要释放相同屡次的锁。
以上三种类型的互斥量都是包装了操做系统底层的pthread_mutex_t:
pthread_mutex_t结构多线程

在C++之中并不提倡咱们直接对锁进行操做,由于在lock以后忘记调用unlock很容易形成死锁。而对临界资源进行操做时,可能会抛出异常,程序也有可能break,return 甚至 goto,这些状况都极容易致使unlock没有被调用。因此C++之中经过RAII来解决这个问题,它提供了一系列的通用管理互斥量的类:并发

互斥量管理 版本 做用
lock_graud C++11 基于做用域的互斥量管理
unique_lock C++11 更加灵活的互斥量管理
shared_lock C++14 共享互斥量的管理
scope_lock C++17 多互斥量避免死锁的管理

建立互斥量管理对象时,它试图给给定mutex加锁。当程序离开互斥量管理对象的做用域时,互斥量管理对象会析构而且并释放mutex。因此咱们则不须要担忧程序跳出或产生异常引起的死锁了。
对于须要加锁的代码段,能够经过{}括起来造成一个做用域。好比上述代码的栗子,能够进行以下改写(推荐):工具

long num = 0;
std::mutex num_mutex;

void numplus() {
    std::lock_guard<std::mutex> lock_guard(num_mutex);
    for (long i = 0; i < 1000000; ++i) {
        num++;
    }
};
void numsub() {
    std::lock_guard<std::mutex> lock_guard(num_mutex);
    for (long i = 0; i < 1000000; ++i) {
        num--;
    }
}

int main() {
    std::thread t1(numplus);
    std::thread t2(numsub);
    t1.join();
    t2.join();
    std::cout << num << std::endl;
}

由上述代码能够看到,代码结构变得更加明晰了,对于锁的管理也交给了程序自己来进行处理,减小了出错的可能。性能

shared_mutex

C++14的版本以后提供了共享互斥量,它的区别就在于提供更加细粒度的加锁操做:lock_sharedlock_shared是一个获取共享锁的操做,而lock是一个获取排他锁的操做,经过这种方式更加细粒度化锁的操做。shared_mutex也是基于操做系统底层的读写锁pthread_rwlock_t的封装:学习

pthread_rwlock_t的结构

这里有个事情挺奇怪的,C++14提供了shared_timed_mutex 而在C++17提供了shared_mutex。其实shared_timed_mutex涵盖了shard_mutex的功能。(不知道是否是由于名字被diss了,因此后续在C++17里将shared_mutex**加了回来)。共享互斥量适用与读多写少的场景,举个栗子:

long num = 0;
std::shared_mutex num_mutex;

// 仅有单个线程能够写num的值。
void numplus() {
    std::unique_lock<std::shared_mutex> lock_guard(num_mutex);
    for (long i = 0; i < 1000000; ++i) {
        num++;
    }
};

// 多个线程同时读num的值。
long numprint() {
    std::shared_lock<std::shared_mutex> lock_guard(num_mutex);
    return num;
}

简单来讲:

  • shared_lock是读锁。被锁后仍容许其余线程执行一样被shared_lock的代码
  • unique_lock是写锁。被锁后不容许其余线程执行被shared_lock或unique_lock的代码。它能够同时限制unique_lock与share_lock

不得不说,C++11没有将共享互斥量集成进来,在不少读多写少的应用场合之中,标准库自己提供的锁机制显得很鸡肋,也从而致使了笔者最终只能求助与boost的解决方案。(其实也能够经过标准库的mutex来实现一个读写锁,这也是面试笔试之中经常问到的问题。不过太麻烦了,还得考虑和互斥量管理类兼容什么的,果断放弃啊)

多锁竞争

还剩下最后一个要写的内容:scope_lock ,当咱们要进行多个锁管理时,很容易出现问题,因为加锁的前后顺序不一样致使死锁。(其实原本不想写了,好累。这里就简单用例子作解释吧,偷个懒~~)
以下栗子,加锁顺序不当致使死锁:

std::mutex m1, m2;
// thread 1
{
  std::lock_guard<std::mutex> lock1(m1);
  std::lock_guard<std::mutex> lock2(m2);
}
// thread 2
{
  std::lock_guard<std::mutex> lock2(m2);
  std::lock_guard<std::mutex> lock1(m1);
}

而经过C++17提供的scope_lock就能够很简单解决这个问题了:

std::mutex m1, m2;
// thread 1
{
  std::scope_lock lock(m1, m2);
}
// thread 2
{
  std::scope_lock lock(m1, m2);
}

好吧,妈妈不再用担忧我会死锁了~~

3.小结

算是简单的梳理完C++标准库之中的mutex了,也经过一些栗子比较完整的展示了使用方式。笔者上述关于标准库的内容,在boost库之中都能找到对应的实现,不过若是可以使用标准库,尽可能仍是不要引用boost了。(走投无路的时候记得求助boost,真香~~)但愿你们在实践之中能够很好的运用好这些C++互斥量来更好的确保线程安全了。后续笔者还会继续深刻的探讨有关C++多线程的相关内容,欢迎你们多多指教。

相关文章
相关标签/搜索