C++11中定义的data race是“不同线程中的两个互相冲突的动作,其中至少有一个不是atomic的,而且无一个动作发生在另一个动作之前”。data race总会导致不可预期的行为。在C++11以前,并不能保证“不同的对象拥有各自的内存区”,也就是,在C++98/C++03这个标准是针对单线程的进程的标准,严格来讲,从C++11之前,并行处理不同的对象也可能会导致不可预期的行为。这里只是谈C++11以后的并发。并发往往是针对相同数据,这里的相同数据是说,使用相同的内存区的数据,从C++11开始,一般变量都能保证拥有自己的内存区,但是bitfield例外,不同的bitfield有可能共享同一块内存区。
volatile
与java不同:
- volatile在C++中是一个关键字,主要用来阻止“过度优化”
- 在java中,volatile提供了某系atomicity和order的一些保证
在C++中,volatile只保证对变量的读取的load内存的操作不会被编译器优化,其他的优化几乎没有,并没有像java中提供类似于atomicity和order的保证。
Mutex和Lock
处理并发通常用到的就是锁和互斥量,c++11的标准中提供了需要的互斥量,这里简单演示一下用法,具体的内容后续再说:
本次主要是讨论关于互斥量的使用。首先看一个建的例子:
#include <iostream>
#include <thread>
#include <atomic>
#include <mutex>
using namespace std;
int val=20;
std::mutex valMutex; //control exclusive access to val
int func(int argc){
//thread 1 add
valMutex.lock();
val += 2;
cout << "func : " << val << endl;
valMutex.unlock();
return 0;
}
int foo(int argc){
//thread 2
valMutex.lock();
val *= 2;
cout << "foo : " << val << endl;
valMutex.unlock();
return 0;
}
int main()
{
val = 10;
valMutex.lock(); //保证thread 是在线程1、2前输出
thread t1(func, 0);
thread t2(foo, 0);
cout << "thread:" << endl;
valMutex.unlock();
t1.join();
t2.join();
system("pause");
return 0;
}
上面的代码在10行的位置声明看一个互斥量valMutex,并通过该互斥量实现读写的互斥,但是上面的做法有很多的隐患,我们如果程序在加锁以后抛出了异常,那么这个锁有可能永远无法被获取,标准中提供了一种利用代码域的概念使用局部变量的形式进行加锁和解锁操作。
但是并不推荐这么使用,这么使用不符合RAII的规范,建议使用基于RAII规范,你会发现C++11为你提供了这一个规范,我们不妨使用lock_guard
这个模板,如下:
// Concurr001.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <vector>
#include <chrono>
#include <thread>
#include <future>
#include <atomic>
#include <mutex>
#include <condition_variable>
using namespace std;
std::mutex sMutex;
int solve()
{
{
std::lock_guard<mutex> lg(sMutex);
/*
需要互斥的代码
*/
}//释放锁
}
上面的程序利用了RAII的机制(关于RAII机制的问题,我们通过一个专门的地方进行详细的分析,其实是个很有用,很有趣的东西),避免了因为异常跳出函数而不能或者忘记释放锁带来的问题。这种思想同样也应用在了智能指针中,RAII的使用,确实很有趣。
其实,我们去看这些源码,甚至我们可以想象在其中干了些什么,mutex本身有四种可供选择:
- mutex
- recursove_mutex
- timed_mutex
- recursive_timed_mutex
对应的lock的操作也有四种:
- lock()
: 捕获mutex,否则阻塞
- try_lock()
: 捕获mutex,不成功返回false
- unlock()
: 释放锁定的互斥量
- try_lock_for()
: (适用于timed_mutex
和recursive_timed_mutex
)试着在时间段内捕获一个lock
- try_lock_until()
:(适用于timed_mutex
和recursive_timed_mutex
)试着捕获一个lock知道某个时间点
recursove_mutex
和recursive_timed_mutex
是支持多个lock的,其余的不行。class std::timed_mutex
额外允许你传递一个时间段或者时间点,用来定义多长时间内他可以尝试捕获一个lock,为它提供了try_lock_for()
和try_lock_until()
。recursive_timed_mutex
允许一个线程多次取得其lock
我们并不会经常使用mutex的成员函数中的这些操作,如果上面提到了,我们经常会用一点小技巧,使用RAII来完成。C++11为此提供了:
- lock_guard
- unique_lock
lock_guard
提供了如下的操作:
操作 | 效果 |
---|---|
lock_guard lg(m) |
为mutex m建立一个lock guard并锁定之 |
lock_guard lg(m, adopt_lock) |
为已经被锁定的mutex m见了一个 lock guard |
lg.~lock_guard() |
解锁mutex并销毁lock guard |
unique_lock
的操作略多一些
操作 | 效果 |
---|---|
unique_lock l |
default构造函数,建立一个lock但不关联任何mutex |
unique_lock l(m) |
为mutex m 建立一个lock guard并锁定它 |
unique_lock l(m,adopt_lock) |
为已锁定的mutex m建立一个lock guard |
unique_lock l(m,defer_lock) |
为mutex m建立一个lock guard,但不锁定它 |
unique_lock l(m,try_lock) |
为mutex m建立一个lock guard, 并试图锁定它 |
unique_lock l(m,dur) |
为mutex m建立一个lock guard,并试图在时间段dur内锁定它 |
unique_lock l(m,tp) |
为mutex m建立一个lock guard,并试图锁定直到时间点tp |
unique_lock l(rv) |
move构造函数 |
if(l) |
检查关联的mutex是否被锁定 |
l.mutex() |
返回一个指针指向关联的mutex |
l.lock() |
锁定 |
l.try_lock() |
尝试锁定,如果成功返回true |
l.try_lock_for(dur) |
尝试在时间段dur内锁定关联的mutex,如果成功 |