C++11 并发编程教程&学习笔记
参考:
1、原文:http://baptiste-wicht.com/posts/2012/03/cpp11-concurrency-part1-start-threads.html
为了能够编译本文的示例代码,你需要有一个支持 C++11 的编译器,笔者使用的是 TDM-GCC4.9.2
并且需要添加 “-std=c++11” 或 “-std=c++0x” 编译选项以启动 GCC 对 C++11 的支持。win7+codeblocks的配置可见:http://blog.youkuaiyun.com/huhaijing/article/details/51753085
C++11 并发编程教程 - Part 1 : thread 初探
启动线程
创建一个 std::thread 的实例时,便会自行启动。
创建线程实例时,必须提供该线程将要执行的函数,方法之一是传递一个函数指针。
#include <thread>
#include <iostream>
using std::cout;
using std::endl;
using std::thread;
void hello(){
cout << "Hello from thread" << endl;
}
int main(){
thread t1(hello);
t1.join(); // 直到t1结束后,main才返回,若不加该句,则可能t1未完成main就返回了
return 0;
}
所有的线程工具均置于头文件 thread 中。
这个例子中值得注意的是对函数 join() 的调用。该调用将导致当前线程等待被 join 的线程结束(在本例中即线程 main 必须等待线程 t1 结束后方可继续执行)。如果你忽略掉对 join() 的调用,其结果是未定义的 —— 程序可能打印出 “Hello from thread” 以及一个换行,或者只打印出 “Hello from thread” 却没有换行,甚至什么都不做,那是因为线程main 可能在线程 t1 结束之前就返回了。
区分线程
每个线程都有唯一的 ID 以便我们加以区分。使用 std::thread 类的 get_id() 便可获取标识对应线程的唯一 ID。
使用 std::this_thread 来获取当前线程的引用。下面的例子将创建一些线程并使它们打印自己的 ID:
#include <thread>
#include <iostream>
#include <vector>
using namespace std;
void hello(){
// std::this_thread 当前线程的引用
// get_id()获取标识进程线程的唯一ID
cout << "Hello from thread" << this_thread::get_id() << endl;
}
int main(){
vector<thread> threads;
for(int i = 0; i < 5; ++i){
threads.push_back(thread(hello));
}
for(auto& th : threads){
th.join();
}
return 0;
}
线程之间存在 interleaving ,某个线程可能随时被抢占。
又因为输出到 ostream 分几个步骤(首先输出一个 string,然后是 ID,最后输出换行),
因此一个线程可能执行了第一个步骤后就被其他线程抢占了,直到其他所有线程打印完之后才能进行后面的步骤。
使用Lambda表达式启动线程
当线程所要执行的代码非常短小时,没有必要专门为之创建一个函数,可使用 Lambda表达式。
#include <thread>
#include <iostream>
#include <vector>
using namespace std;
int main(){
vector<thread> threads;
for(int i = 0; i < 5; ++i){
threads.push_back(std::thread(
[](){
cout << "Hello from thread " << this_thread::get_id() << endl;
}));
}
for(auto& th : threads){
th.join();
}
return 0;
}
C++11 并发编程教程 - Part 2 : 保护共享数据
上面的代码中,线程互相独立,通常情况下,线程之间可能用到共享数据。一旦对共享数据进行操作,就面临着一个新的问题 —— 同步。
同步问题
我们就拿一个简单的计数器作为示例吧。
这个计数器是一个结构体,他拥有一个计数变量,以及增加或减少计数的函数,看起来像这个样子:
#include <thread>
#include <iostream>
#include <vector>
using namespace std;
struct Counter {
int m_nValue;
Counter(int n = 0) : m_nValue(n){}
void increment(){ ++m_nValue; }
};
int main(){
Counter counter;
vector<thread> threads;
for(int i = 0; i < 5; ++i){
threads.push_back(thread([&counter](){
for(int i = 0; i < 999999; ++i){
counter.increment();
}
}));
}
for(auto& th : threads){
th.join();
}
cout << counter.m_nValue << endl;
return 0;
}
每次运行结果会不同,因为计数器的increment()操作并非原子操作,而是由3个独立的操作组成的:
1、读取m_nValue变量的当前值。
2、将读取的当前值+1
3、将+1后的值写回value变量。
当单线程运行上述代码,每次运行的结果是一样的,上述三个步骤会按顺序进行。但是在多进程情况下,可能存在如下执行顺序:
线程a:读取 m_nValue的当前值,得到值为 0。加1。得到1,但还没来得及写回内存
线程b:读取 m_nValue的当前值,得到值为 0。加1。得到1,但还没来得及写回内存。
线程a:将 1 写回 m_nValue 内存并返回 1。
线程b:将 1 写回 m_nValue内存并返回 1。
这种情况源于线程间的 interleaving(交叉运行)。
Interleaving 描述了多线程同时执行几句代码的各种情况。就算仅仅只有两个线程同时执行这三个操作,也会存在很多可能的 interleaving。当你有许多线程同时执行多个操作时,要想枚举出所有 interleaving,几乎是不可能的。而且如果线程在执行单个操作的不同指令之间被抢占,也会导致 interleaving 的发生。
目前有许多可以解决这一问题的方案:
- Semaphores
- Atomic references
- Monitors
- Condition codes
- Compare and swap
- ……
本文将使用 Semaphores 去解决这一问题。
事实上,我们仅仅使用了Semaphores 中比较特殊的一种 —— 互斥量。
互斥量是一个特殊的对象,在同一时刻只有一个线程能够得到该对象上的锁。借助互斥量这种简而有力的性质,我们便可以解决线程同步问题。
使用互斥量保证 Counter 的线程安全
在 C++11 的线程库中,互斥量被放置于头文件 mutex,并以 std::mutex 类加以实现。互斥量有两个重要的函数:lock() 和 unlock()。顾名思义,前者使当前线程尝试获取互斥量的锁,后者则释放已经获取的锁。lock() 函数是阻塞式的,线程一旦调用 lock(),就会一直阻塞直到该线程获得对应的锁。
为了使计数器具备线程安全性,我们需要对其添加 std::mutex 成员,并在成员函数中对互斥量进行 lock()和unlock() 调用。
struct Counter {
int m_nValue;
mutex mtx;
Counter() : m_nValue(0){}
void increment(){
mtx.lock();
++m_nValue;
mtx.unlock();
}
};
如果我们现在再次运行之前的测试程序,我们将始终得到正确的输出。(加锁之后,会大大降低程序运行效率,所以可以将上面的内循环降低至10000)
异常与锁
现在让我们来看看另外一种情况会发生什么。
假设现在我们的计数器拥有一个 derement() 操作,当 value 被减为 0 时抛出一个异常:
struct Counter {
int m_nValue;
Counter() : m_nValue(0){}
void increment(){
++m_nValue;
}
void decrement(){
if(m_nValue == 0){
throw "Value cannot be less than 0";
}
--m_nValue;
}
};
假设你想在不更改上述代码的前提下为其提供线程安全性,那么你需要为其创建一个 Wrapper 类:
struct ConcurrentCounter{
mutex mtx;
Counter counter;
void increment(){
mtx.lock();
counter.increment();
mtx.unlock();
}
void decrement(){
mtx.lock();
counter.decrement();
mtx.unlock();
}
};
这个 Wrapper 将在大多数情况下正常工作,然而一旦 decrement() 抛出异常,你就遇到大麻烦了,当异常被抛出时,unlock() 函数将不会被调用,这将导致本线程获得的锁不被释放,你的程序也就顺理成章的被永久阻塞了。为了修复这一问题,你需要使用 try/catch 块以保证在抛出任何异常之前释放获得的锁。
void decrement(){
mtx.lock();
try{
counter.decrement();
} catch(string e) {
mtx.unlock();
throw e;
}
mtx.unlock();
}
代码并不复杂,但是看起来却很丑陋。试想一下,你现在的函数拥有 10 个返回点,那么你就需要在每个返回点前调用 unlock() 函数,而忘掉其中的某一个的可能性是非常大的。更大的风险在于你又添加了新的函数返回点,却没有对应地添加 unlock()。下一节将给出解决此问题的好办法。
锁的自动管理
当你想保护整个代码段(就本文而言是一个函数,但也可以是某个循环体或其他控制结构[即一个作用域])免受多线程的侵害时,有一个办法将有助于防止忘记释放锁:std::lock_guard。
这个类是一个简单、智能的锁管理器。当 std::lock_guard 实例被创建时,它自动地调用互斥量的lock() 函数,当该实例被销毁时,它也顺带释放掉获得的锁。你可以像这样使用它:
struct ConcurrentSafeCounter{
mutex mtx;
Counter counter;
void increment(){
lock_guard<mutex> guard(mtx);
counter.increment();
}
void decrement(){
lock_guard<mutex> guard(mtx);
counter.decrement();
}
};
代码变得更整洁了不是吗?
使用这种方法,你无须绷紧神经关注每一个函数返回点是否释放了锁,因为这个操作已经被std::lock_guard 实例的析构函数接管了。
注意
现在我们结束了短暂的 Semaphores 之旅。在本章中你学习了如何使用 C++ 线程库中的互斥量来保护你的共享数据。**
但有一点请牢记:锁机制会带来效率的降低。的确,一旦使用锁,你的部分代码就变得有序[非并发]了。如果你想要设计一个高度并发的应用程序,你将会用到其他一些比锁更好的机制,但他们已不属于本文的讨论范畴。
C++11 并发编程教程 - Part 3 : 锁的进阶与条件变量
上一章使用互斥量解决线程同步,这一章将进一步讨论互斥量的话题,并介绍C++11并发库中的另一种同步机制——条件变量。
递归锁
待续。。。