C++ 11/14/17 线程资源同步对象 在 C/C 语言中直接使用操作系统提供的多线程资源同步 API 虽然功能强大但毕竟存在诸多限制且同样的代码却不能同时兼容 Windows 和 Linux 两个平台再者 C/C 这种传统语言的使用份额正在被 Java、python、go 等语言慢慢蚕食很大一部分原因是 C/C 这门编程语言在一些功能上缺少“完备性”如对线程同步技术的支持而这些功能在像 Java、python、go 中是标配。因此 C 11 标准新加入了很多现代语言标配的东西其中线程资源同步对象就是其中很重要的一部分。本小节将讨论 C 11 标准中新增的用于线程同步的std::mutex和std::condition_variable对象的用法有了它们我们就可以写出跨平台的多线程程序了。std::mutex 系列关于 mutex 的基本概念上文已经介绍过了这里不再赘述。C 11/14/17 中提供了如下 mutex 系列类型互斥量版本作用mutexC11最基本的互斥量timed_mutexC11有超时机制的互斥量recursive_mutexC11可重入的互斥量recursive_timed_mutexC11结合 timed_mutex 和 recursive_mutex 特点的互斥量shared_timed_mutexC14具有超时机制的可共享互斥量shared_mutexC17共享的互斥量这个系列的对象均提供了加锁lock、尝试加锁trylock和解锁unlock的方法我们以std::mutex为例来看一段示例代码#include iostream #include chrono #include thread #include mutex // protected by g_num_mutex int g_num 0; std::mutex g_num_mutex; void slow_increment(int id) { for (int i 0; i 3; i) { g_num_mutex.lock(); g_num; std::cout id g_num std::endl; g_num_mutex.unlock(); //sleep for 1 second std::this_thread::sleep_for(std::chrono::seconds(1)); } } int main() { std::thread t1(slow_increment, 0); std::thread t2(slow_increment, 1); t1.join(); t2.join(); return 0; }上述代码中创建了两个线程t1和t2在线程函数的 for 循环中调用 std::mutex.lock() 和 std::mutex.unlock() 对全局变量g_num进行保护。编译程序并输出结果如下[rootlocalhost testmultithread]# g -g -o mutex c11mutex.cpp -stdc0x -lpthread [rootlocalhost testmultithread]# ./mutex 0 1 1 2 0 3 1 4 1 5 0 6注意如果你在 Linux 下编译和运行程序在编译时你需要链接 pthread 库否则能够正常编译但是运行时程序会崩溃崩溃原因terminate called after throwing an instance of std::system_error what(): Enable multithreading to use std: Operation not permitted为了避免死锁std::mutex.lock()和std::mutex::unlock()方法需要成对使用但是如上文介绍的如果一个函数中有很多出口而互斥体对象又是需要在整个函数作用域保护的资源那么在编码时因为忘记在某个出口处调用std::mutex.unlock而造成死锁上文中推荐使用利用 RAII 技术封装这两个接口其实 C 11 标准也想到了整个问题因为已经为我们提供了如下封装互斥量管理版本作用lock_guardC11基于作用域的互斥量管理unique_lockC11更加灵活的互斥量管理shared_lockC14共享互斥量的管理scope_lockC17多互斥量避免死锁的管理我们这里以std::lock_guard为例void func() { std::lock_guardstd::mutex guard(mymutex); //在这里放被保护的资源操作 }mymutex 的类型是 std::mutex在guard对象的构造函数中会自动调用 mymutex.lock() 方法加锁当该函数出了作用域后调用guard对象时析构函数时会自动调用 mymutex.unlock() 方法解锁。注意mymutex 生命周期必须长于函数 func 的作用域很多人在初学这个利用 RAII 技术封装的 std::lock_guard 对象时可能会写出这样的代码//错误的写法这样是没法在多线程调用该函数时保护指定的数据的。 void func() { std::mutex m; std::lock_guardstd::mutex guard(m); //在这里放被保护的资源操作 }另外如果一个 std::mutex 对象已经调用了lock()方法再次调用时其行为是未定义的这是一个错误的做法。所谓“行为未定义”即在不同平台上可能会有不同的行为。#include mutex int main() { std::mutex m; m.lock(); m.lock(); m.unlock(); return 0; }实际测试时上述代码重复调用std::mutex.lock()方法在 Windows 平台上会引起程序崩溃。如下图所示上述代码在 Linux 系统上运行时会阻塞在第二次调用 std::mutex.lock() 处验证结果如下[rootlocalhost testmultithread]# g -g -o mutexlock mutexlock.cpp -stdc0x -lpthread [rootlocalhost testmultithread]# gdb mutexlock Reading symbols from /root/testmultithread/mutexlock...done. (gdb) r Starting program: /root/testmultithread/mutexlock [Thread debugging using libthread_db enabled] Using host libthread_db library /lib64/libthread_db.so.1. ^C Program received signal SIGINT, Interrupt. 0x00007ffff7bcd4ed in __lll_lock_wait () from /lib64/libpthread.so.0 Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7.x86_64 libgcc-4.8.5-36.el7.x86_64 libstdc-4.8.5-36.el7.x86_64 (gdb) bt #0 0x00007ffff7bcd4ed in __lll_lock_wait () from /lib64/libpthread.so.0 #1 0x00007ffff7bc8dcb in _L_lock_883 () from /lib64/libpthread.so.0 #2 0x00007ffff7bc8c98 in pthread_mutex_lock () from /lib64/libpthread.so.0 #3 0x00000000004006f7 in __gthread_mutex_lock (__mutex0x7fffffffe3e0) at /usr/include/c/4.8.2/x86_64-redhat-linux/bits/gthr-default.h:748 #4 0x00000000004007a2 in std::mutex::lock (this0x7fffffffe3e0) at /usr/include/c/4.8.2/mutex:134 #5 0x0000000000400777 in main () at mutexlock.cpp:7 (gdb) f 5 #5 0x0000000000400777 in main () at mutexlock.cpp:7 7 m.lock(); (gdb) l 2 3 int main() 4 { 5 std::mutex m; 6 m.lock(); 7 m.lock(); 8 m.unlock(); 9 10 return 0; 11 } (gdb)我们使用 gdb 运行程序然后使用 bt 命令看到程序确实阻塞在第二个 m.lock() 的地方代码第7行。不管怎样对一个已经调用 lock() 方法再次调用 lock() 方法的做法是错误的我们实际开发中要避免这么做。std::condition_variableC 11 提供了std::condition_variable这个类代表条件变量与 Linux 系统原生的条件变量一样同时提供了等待条件变量满足的wait系列方法wait、wait_for、wait_until 方法发送条件信号使用notify方法notify_one和notify_all方法当然使用std::condition_variable对象时需要绑定一个std::unique_lock或std::lock_guard对象。C 11 中 std::condition_variable 不再需要显式调用方法初始化和销毁。我们将上文中介绍 Linux 条件变量的例子改写成 C 11 版本#include thread #include mutex #include condition_variable #include list #include iostream class Task { public: Task(int taskID) { this-taskID taskID; } void doTask() { std::cout handle a task, taskID: taskID , threadID: std::this_thread::get_id() std::endl; } private: int taskID; }; std::mutex mymutex; std::listTask* tasks; std::condition_variable mycv; void* consumer_thread() { Task* pTask NULL; while (true) { std::unique_lockstd::mutex guard(mymutex); while (tasks.empty()) { //如果获得了互斥锁但是条件不合适的话pthread_cond_wait会释放锁不往下执行。 //当发生变化后条件合适pthread_cond_wait将直接获得锁。 mycv.wait(guard); } pTask tasks.front(); tasks.pop_front(); if (pTask NULL) continue; pTask-doTask(); delete pTask; pTask NULL; } return NULL; } void* producer_thread() { int taskID 0; Task* pTask NULL; while (true) { pTask new Task(taskID); //使用括号减小guard锁的作用范围 { std::lock_guardstd::mutex guard(mymutex); tasks.push_back(pTask); std::cout produce a task, taskID: taskID , threadID: std::this_thread::get_id() std::endl; } //释放信号量通知消费者线程 mycv.notify_one(); taskID ; //休眠1秒 std::this_thread::sleep_for(std::chrono::seconds(1)); } return NULL; } int main() { //创建5个消费者线程 std::thread consumer1(consumer_thread); std::thread consumer2(consumer_thread); std::thread consumer3(consumer_thread); std::thread consumer4(consumer_thread); std::thread consumer5(consumer_thread); //创建一个生产者线程 std::thread producer(producer_thread); producer.join(); consumer1.join(); consumer2.join(); consumer3.join(); consumer4.join(); consumer5.join(); return 0; }编译并执行程序输出结果如下所示[rootlocalhost testmultithread]# g -g -o cpp11cv cpp11cv.cpp -stdc0x -lpthread [rootlocalhost testmultithread]# ./cpp11cv produce a task, taskID: 0, threadID: 140427590100736 handle a task, taskID: 0, threadID: 140427623671552 produce a task, taskID: 1, threadID: 140427590100736 handle a task, taskID: 1, threadID: 140427632064256 produce a task, taskID: 2, threadID: 140427590100736 handle a task, taskID: 2, threadID: 140427615278848 produce a task, taskID: 3, threadID: 140427590100736 handle a task, taskID: 3, threadID: 140427606886144 produce a task, taskID: 4, threadID: 140427590100736 handle a task, taskID: 4, threadID: 140427598493440 produce a task, taskID: 5, threadID: 140427590100736 handle a task, taskID: 5, threadID: 140427623671552 produce a task, taskID: 6, threadID: 140427590100736 handle a task, taskID: 6, threadID: 140427632064256 produce a task, taskID: 7, threadID: 140427590100736 handle a task, taskID: 7, threadID: 140427615278848 produce a task, taskID: 8, threadID: 140427590100736 handle a task, taskID: 8, threadID: 140427606886144 produce a task, taskID: 9, threadID: 140427590100736 handle a task, taskID: 9, threadID: 140427598493440 ...更多输出结果省略...需要注意的是如果在 Linux 平台上std::condition_variable 也存在虚假唤醒这一现象如何避免与上文中介绍 Linux 原生的条件变量方法一样。