添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
C++多线程并发编程 3. 数据争用和互斥锁(C++如何保证线程安全)Data Race and Mutex

C++多线程并发编程 3. 数据争用和互斥锁(C++如何保证线程安全)Data Race and Mutex

1 年前 · 来自专栏 C++ 多线程并发编程笔记

整个这一节,都是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++)