C++多线程并发编程 3. 数据争用和互斥锁(C++如何保证线程安全)Data Race and Mutex
整个这一节,都是C++保证线程安全的方法!!!
Racing condition 竞争冒险(又名竞态条件)
racing condition 是这样的一种condition,程序的输出依赖于一个或多个线程的执行顺序。
最常见的例子是多个线程同时向同一个cout输出。
竞态条件通常不利于我们的编程,我们需要避免。
就像下面这样,竞态条件会使得两个线程交替打印,打出来的信息会混到一起。
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
void func1() {
for (int i = 0; i > -100; i--)
cout << "Here is t1 thread: "<< i << endl;
int main() {
thread t1(func1);
for (int i = 0; i < 100; i++)
cout << "Here is main thread: " << i << endl;
t1.join();
return 0;
解决竞态条件,可以理解为进程同步的方法。
1. 解决竞态条件:使用mutex(互斥锁)
使用mutex,可以使得多个进程独占地使用同一个资源。
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
mutex mu;
void shared_print(string msg, int id) {
mu.lock();
cout << msg << " " << id << endl;
mu.unlock();
void func1() {
for (int i = 0; i > -100; i--)
shared_print("Here is t1 thread: ", i);
int main() {
thread t1(func1);
for (int i = 0; i < 100; i++)
shared_print("Here is main thread: ", i);
t1.join();
return 0;
每次调用cout时都使用mutex mu,就能使得每个时间点都只有一个线程在使用mutex,就能防止每行打印混乱。
(注意:可能要测很多次才能看出数据混乱来,我用下面的测试用例,不算小的数据量,测了很多次,才出现的加锁和不加锁的区别。)
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
mutex mu;
void shared_print(string msg, int id, string c) {
// mu.lock();
cout << c << endl;
// mu.unlock();
void func1() {
string s(50, 'b');
for (int i = 0; i > -100; i--)
shared_print("Here is t1 thread: ", i, s);
cout << endl;
int main() {
thread t1(func1);
string s(50, 'a');
for (int i = 0; i < 100; i++)
shared_print("Here is main thread: ", i, s);
cout << endl;
t1.join();
return 0;
加锁的话,不会串行,不加锁的话,有很少的串行。
2. mutex的缺陷→使用lock_guard
回到上面的例子。如果mutex.lock()和mutex.unlock()之间的代码出现异常的话,那么mutex就不会被unlock,就会死锁了。 因此,不推荐直接使用mutex。
因此,推荐使用
std::lock_guard<std::mutex> guard(mu);
lock_guard
像下面这样。
void shared_print(string msg, int id) {
std::lock_guard<std::mutex> guard(mu); // RAII
cout << msg << id << endl;
这样,无论guard定义之后的语句是否会发生异常,都会导致guard离开其作用域。
而在guard变量离开其作用域时,会被销毁,此时一定会调用mu.unlock().
因此,这样就不会因为mutex申请和释放之间的语句异常而导致的mutex一直不被释放的死锁。
3. 绑定mutex到某个特定资源
上面的方法其实也有缺陷。我们只是使用mutex使得调用某个函数的线程保持同步,不争用目标资源。
但是有的目标资源是全局的,可能被多个线程使用,比如上面的cout。
我们只是同步了使用cout的一个函数,但并不能保证所有其他线程访问目标资源时都是同步的(比如cout)。
为了解决这个问题,我们可以将mutex绑定到某一个特定的资源,用一个mutex控制该资源的非争用。
像下面这样,我们创建一个类,其中包括私有成员,一个共享流f,一个互斥锁m_mutex。
通过提供给外界的接口shared_print,可以保证外界要使用共享资源f,一定要通过lock_guard类型locker。因此,这样将mutex与一个资源相绑定到一个类中,就实现了互斥锁的全面保护。
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
class LogFile {
std::mutex m_mutex;
ofstream f;
public:
LogFile() {
f.open("log.txt");
} // 这里应该有一个析构函数用来close,此处先忽略
void shared_print(string id, int value) {
std::lock_guard<mutex> locker(m_mutex);
f << "From" << id << ": " << value << endl;
// Never return f to the outside world
ofstream& getStream() {return f;}
// Never pass f as an argument to user provided function
void processf(void fun(ofstream&)) {
fun(f);
void func1(LogFile& log) {
for (int i = 0; i > -100; i--)
log.shared_print("Here is t1 thread: ", i);
cout << endl;
int main() {
LogFile log;
thread t1(func1, std::ref(log));
for (int i = 0; i < 100; i++)