C++多线程快速入门(二)共享数据同步以及数据竞争

std::unique_lock类模板

互斥锁保证了线程间的同步,却将并行操作变成了串行操作,对性能有较大影响,所以我们要尽可能减小锁的区间粒度。
lock_guard只能保证在析构的时候进行解锁操作,所以其本身并没有提供加锁解锁的接口

class logFile{
    
    
private:
    std::mutex mtx;
    ofstream f;
public:
    logFile() {
    
    
        f.open("log.txt");
    }
    ~logFile() {
    
    
        f.close();
    }
    void sharedPrint(string msg, int id) {
    
    
        {
    
    
            std::lock_guard<std::mutex> guard(mtx);
            // do sth1
        }
        // do sth2
        // ...
        {
    
    
            std::lock_guard<std::mutex> guard(mtx);
            // do sth3
            f << msg << id << endl;
            cout << msg << id << endl;
        }
    }
};

上面代码中,sharedPrint函数内部有两段代码需要进行加锁保护,此时使用lock_guard就需要创建两个局部对象来管理同一个互斥锁。
其实可以使用unique_lock,它提供了**lock()unlock()**接口,能记录现在处于上锁还是没上锁状态,在析构的时候,会根据当前状态来决定是否要进行解锁(lock_guard就一定会解锁)

    void sharedPrint(string msg, int id) {
    
    

        std::unique_lock<std::mutex> guard(mtx);
        // do sth1
        guard.unlock(); // 临时解锁
        // do sth2
        guard.lock(); // 继续上锁
        // do sth3
        f << msg << id << endl;
        cout << msg << id << endl;
        // guard.unlock(); // 这个可要可不要,因为析构的时候也会自动执行的
    }

仅调用一次

程序免不了要初始化数据,线程并发没有某种同步手段来控制,会导致初始化函数多次运行。

c++11种提供仅调用一次的功能,首先声明一个once_flag类型的变量作为初始化标志,最好是静态、全局的,这样对于线程就是可见的了。然后调用专门的call_once()函数,以函数式编程的方式,传递这个标志和初始化函数,这样就可以保证,即使多个线程重入call_once(),也只能由一个线程会成功初始化

#include <iostream>
#include <thread>
#include <mutex>

// 全局的初始化标志
static std::once_flag flag;
auto f = []()
{
    
    
    std::call_once(flag,
                   [](){
    
    
                       std::cout << "only once" << std::endl;
                   }
    );
};
int main() {
    
    
   // 启动两个线程,运行函数f
   std::thread t1(f);
   std::thread t2(f);
   t1.join();
   t2.join();
   // std::cout << "finished" << std::endl;
   return 0;
}

实际编译结果如下:

only once

线程局部存储

当读写全局变量时,一般也会出现数据竞争,但是全局变量不一定是必须共享的,可能仅仅是为了方便线程传入传出数据,不是共享所有权。此时可以使用关键字thread_local实现线程局部缓存,被其标志的变量在每个线程里都会有一个独立的副本。

#include <iostream>
#include <thread>

int main() {
    
    
   // 启动两个线程,运行函数f
    thread_local int n = 0;
    auto f = [&](int x)
    {
    
    
        n += x;
        std::cout << n << std::endl;
    };
   std::thread t1(f, 10);
   std::thread t2(f, 20);
   t1.join();
   t2.join();
   // std::cout << "finished" << std::endl;
   return 0;
}

结果是:

10
20

说明两个线程互不干扰,如果将变量声明改为static,两个线程会共享这个变量,导致连续加两次,结果是下面:

10 or 20
30    30

原子变量

对于非独占、必须共享的数据,要保证多线程读写共享数据一致就要解决同步问题,两个线程得互斥。但是mutex互斥量成本较高,可以使用atmoic原子化操作。

int main() {
    
    
   static std::atomic_flag flag {
    
    false};    // 原子化的标志量
   static atomic_int n;     // 原子化的int
   auto f = [&]()
   {
    
    
       auto value = flag.test_and_set();        // TAS检查原子标志量
       if (value) {
    
    
           std::cout << "flag has been set" << std::endl;
       } else {
    
    
           std::cout << "set flag by" << std::this_thread::get_id() << std::endl;   // 输出线程id
       }
       n += 100;    // 原子变量加法运算
       std::this_thread::sleep_for(std::chrono::seconds(5));    //线程睡眠
       std::cout << n << std::endl;
   };
    std::thread t1(f);
    std::thread t2(f);

    t1.join();
    t2.join();
}

往期内容回顾

C++多线程快速入门(一):基本&常用操作

おすすめ

転載: blog.csdn.net/qq_42604176/article/details/120629556