C++ std::Thread多线程和mutex锁通俗易懂

2023-11-17

记录学习过程,如有新的发现,随时补充,如有错误或补充,请各位大佬指正。

一、前言

多线程有多种方式。std::Thread、boost::Thread、pthread、Windows库等。本文只关注std::Thread,可以跨平台运行。

二、std::Thread

thread对象构造

//头文件
#include <thread> 

//函数指针,void thread_fun()
std::thread t1 ( thread_fun);

//函数对象 void thread_fun(int x)
std::thread t2 ( thread_fun(100));

//lamda表达式
std::thread th3_lamda([](int x) {
        for (int i=0; i < x; ++i) {
            std::cout << std::this_thread::get_id() << "," << i << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1 ));
        }
}, 5);


//直接创建线程,没有名字void thread_fun(int x)
std::thread (thread_fun,1).join();

//类成员函数
std::thread myThread(&MyClass::threadFunction, &obj);
//obj为对应具体的对象。void MyClass::threadFunction();

thread常见成员函数

函数 作用
join() 等待线程结束并清理资源(人话:等他结束了再执行主线程)
joinable() 返回线程是否可以执行join
detach() 不影响主线程运行,必须线程创建时立即调用,函数不能join
get.id() 获取线程ID
operator= 移动构造函数

thread传递参数

//传递值
int a = 10;
std::thread(thread_fun , a);
//函数void thread_fun (int a);
//传递引用
int a = 10;
std::thread(thread_fun , std::ref(a));
//函数void thread_fun (int &a);
//常量引用
std::thread(thread_fun , std::cref(a));

三、mutex互斥锁

如果多个线程有共同需要使用的资源,避免冲突,要加上锁,一次只有一个线程能够使用这个资源。

头文件#include <mutex>

1、mutex基本锁

lock() 将mutex上锁,如果已经被其他线程上锁,就会阻塞直到解锁
unlock() 解锁mutex,如果没有获得锁的所有权就调用解锁,会发生不可知异常
bool try_lock() 尝试上锁。返回bool。lock不成功也会继续往下进行。

lock_guard和unique_lock的使用和区别

可以使用RAII(通过类的构造析构)来实现更好的编码方式。 RAII:也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证在任何情况下,使用对象时先构造对象,最后析构对象。一般锁尽量用lock_guard。

两者区别:

1、unique_lock与lock_guard都能实现自动加锁和解锁,但是前者更加灵活,能实现更多的功能。lock_guard速度和效率高一点。
2、unique_lock可以进行临时解锁和再上锁,如在构造对象之后使用lck.unlock()就可以进行解锁, lck.lock()进行上锁,而不必等到析构时自动解锁。lock_guard是不支持手动释放的。
3、condition_variable必须与uniqu_lock搭配使用。

 std::unique_lock<std::mutex> lck(mtx);
 std::lock_guard<std::mutex> lck(mtx);
//这种构造方式下没有区别

 unique_lock的三种参数

a、只创建不锁,创建之前得没有锁住mtx
//如果线程没有锁住mutex,则可以先创建不锁。
std::unique_lock<std::mutex> lock(mtx , std::defer_lock);
lock.lock();
lock.unlock();
lock.owns_lock();//返回是否持有当前mtx的所有权
lock.try_lock_for()//锁一段时间
lock.try_lock_until();//锁到什么时间点。
 b、只创建不锁,创建之前得要锁住mtx(感觉用处不多,主要用于传递锁吧)
//`std::unique_lock` 的构造函数中的 `adopt_lock` 参数用于指示该 `unique_lock` 对象在构造时,
//应该假设它已经获得了互斥锁的所有权。换句话说,`adopt_lock` 参数允许你在构造 `unique_lock`
//对象时,将一个已经获得的互斥锁传递给它,而不是在构造时尝试获得互斥锁。

//常见的情况下,使用 `adopt_lock` 参数的情况包括:

//1. 从一个函数或代码块传递已经获得的互斥锁:
//假设在某个函数中已经获得了一个互斥锁,并且你想在函数的返回值中返回一个 `unique_lock` 对象
//同时保持互斥锁的所有权,那么你可以使用 `adopt_lock` 来传递这个已经获得的互斥锁。

   std::unique_lock<std::mutex> functionWithLock()
   {
       std::mutex mtx;
       mtx.lock();
       return std::unique_lock<std::mutex>(mtx, std::adopt_lock);
   }

//2. 用于条件变量中的等待操作:
//当使用 `std::condition_variable` 的 `wait` 函数时,需要传入一个已经获得的互斥锁,
//用于在等待时自动释放互斥锁。这时可以使用 `adopt_lock` 参数来传递已经获得的互斥锁。
   std::condition_variable cv;
   std::mutex mtx;
   std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
  
   // ... 其他代码 ...
  
   // 等待条件变量,并在等待时自动释放互斥锁
   cv.wait(lock);

//需要注意的是,在使用 `adopt_lock` 参数时,你必须确保在传递互斥锁给 `unique_lock` 对象时,
//该互斥锁的所有权确实在你手中,否则会导致未定义的行为。
c、创建时尝试锁,但是锁不成功也继续执行
std::unique_lock<std::mutex>(mtx, std::try_to_lock);

传递unique_lock锁所有权

        std::move(...)  或者  return unique_lock对象

2、 std::time_mutex定时锁

比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until()。

try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时,则返回 false。

try_lock_until 函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时,则返回 false。

3、recursive_mutex递归锁

递归锁允许同一个线程多次获取该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。最后解锁的次数和上锁的次数要相等。递归互斥锁主要用于可能被连续多次上锁(期间未解锁)的情形,例如函数A、B都有加锁逻辑;而在特殊条件下,函数A调用了函数B,则需要用递归锁。最大上锁次数未知,一旦超过最大上锁次数会发生错误。

递归互斥锁的优点是可以避免死锁,缺点是容易出现死循环。

借用一个博客例子:(1条消息) C++ 各种Mutex详解_c++ mutex_游戏开发龙之介的博客-CSDN博客

#include<iostream>
#include <mutex>
#include <thread>
#include<vector>
using namespace std;
recursive_mutex mtx;
vector<char> vec;
static void push_back_c_n_cnt(int n, char c)
{
	mtx.lock();
	if (n == 0)
	{
		mtx.unlock();
		return;
	}
	cout << n <<","<< c << endl;
	vec.push_back(c);
	push_back_c_n_cnt(n-1,c);
	mtx.unlock();

}

int main()
{
	//开启两个线程分别把*和$推进vec
	thread th1(push_back_c_n_cnt, 10, '*');
	thread th2(push_back_c_n_cnt, 10, '$');
	th1.join();
	th2.join();
	cout << " print vec :" << endl;
	//打印vec
	for (int i = 0; i < vec.size(); i++)
	{
		cout << vec[i] << " ";
	}
	puts("");
	cout << "end" << endl;;
	return 0;
}

个人理解:尽量不要使用递归锁,一是效率问题,二是安全问题,会让整个代码更加复杂。上面的代码递归使用push_back,目的是实现push_back字符,那么其实使用一个普通锁,然后for循环push_back进去,再解锁即可。

4、recursive_timed_mutex定时递归锁(略)

5、C++17  shared_lock和shared_mutex共享锁

读锁不独占,可以有几个线程一起读共享变量;写锁独占,有线程写锁时,无论读写都被阻塞。 std::shared_mutex - cppreference.comicon-default.png?t=N6B9https://zh.cppreference.com/w/cpp/thread/shared_mutex

std::shared_mutex mtx;
//共享锁
std::shared_lock<std::shared_mutex> read_lock(mtx);
//独占锁
std::lock_guard<std::shared_mutex> write_lock(mtx);
//或者用unique_lock

这里参考博客的示例,但是我觉得他的示例中用了普通锁,将所有线程都锁住了,一次只有一个线程进行,不能反映出共享锁和独占锁的特点,所以自己修改了一下。每隔1秒写入value,每隔0.5s读取value

(4条消息) C++多线程——读写锁shared_lock/shared_mutex_c++ 读写锁_princeteng的博客-CSDN博客

#include <iostream>
#include <mutex>    //unique_lock
#include <shared_mutex> //shared_mutex shared_lock
#include <thread>

std::mutex mtx;

class ThreadSaferCounter
{
 private:
  mutable std::shared_mutex mutex_;
  int value_ = 0;
 public:
  ThreadSaferCounter(/* args */) {};
  ~ThreadSaferCounter() {};

  int get() const {
    //读者, 获取共享锁, 使用shared_lock
    std::shared_lock<std::shared_mutex> lck(mutex_);//执行mutex_.lock_shared();
    //std::cout << "reader #" << std::this_thread::get_id();
    auto now = std::chrono::system_clock::now();
    auto timestamp = std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count();
    //std::cout << timestamp << " get " << value_ << std::endl;
    std::printf("time %ld get %d \n",timestamp,value_);
    return value_;  //lck 析构, 执行mutex_.unlock_shared();
  }

  int increment() {
    //写者, 获取独占锁, 使用unique_lock
    std::lock_guard<std::shared_mutex> lck(mutex_);//执行mutex_.lock();
    value_++;   //lck 析构, 执行mutex_.unlock();
    //std::cout << "writer #" << std::this_thread::get_id() ;
    auto now = std::chrono::system_clock::now();
    auto timestamp = std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count();
    //std::cout << timestamp << " set " << value_ << std::endl;
    std::printf("time %ld set %d \n",timestamp,value_);
    return value_;
  }

  void reset() {
    //写者, 获取独占锁, 使用unique_lock
    std::lock_guard<std::shared_mutex> lck(mutex_);//执行mutex_.lock();
    value_ = 0;   //lck 析构, 执行mutex_.unlock();
  }
};
ThreadSaferCounter counter;
//每隔1s获取一次
void reader(int id){
  while (true)
  {
    //std::this_thread::sleep_for(std::chrono::seconds(1));
    //std::unique_lock<std::mutex> ulck(mtx);
    //std::cout << "reader #" << id << " get value " << counter.get() << "\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    counter.get();
  }
}

//每隔一秒写入一次
void writer(int id){
  while (true)
  {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    //std::unique_lock<std::mutex> ulck(mtx);
    //std::cout << "writer #" << id << " write value " << counter.increment() << "\n";
    counter.increment();
  }
}

int main()
{
  std::thread rth[5];
  std::thread wth[2];
  for(int i=0; i<5; i++) {
    rth[i] = std::thread(reader, i+1);
  }
  for(int i=0; i<2; i++) {
    wth[i] = std::thread(writer, i+1);
  }

  for(int i=0; i<5; i++) {
    rth[i].join();
  }
  for(int i=0; i<2; i++) {
    wth[i].join();
  }
  return 0;
}

在终端中运行,你就会发现神奇的事情:

然后根据时间戳,你可以看到5个线程的get几乎是同时进行的,相差了大概不到10微秒,而get和set差了100到200多微秒,set之间也是独占的,所以也相差了100多微秒。这个例子很清晰的表示了共享和独占的特点。(为什么不用std::cout流输出呢,因为我发现流输出不是严格按照输出执行完才继续执行后面的代码,会导致多线程cout的时候,一行有多个输出结果。可能这也是为什么google风格的c++建议用printf,具体原因请大佬告知,后面去详细学习一下流输出的原理。)

四、std::condition_variable条件变量

用于满足条件后唤醒线程。比如,线程1执行某任务,线程2阻塞等待通知,不会占用cpu资源,满足一定条件线程1发送通知,线程2接收后继续执行代码,并执行锁。

condition_variable头文件有两个variable类,一个是condition_variable,另一个是condition_variable_any。condition_variable必须结合unique_lock使用。condition_variable_any可以使用任何的锁。

 condition_variable条件变量可以阻塞(wait、wait_for、wait_until)调用的线程直到使用(notify_one或notify_all)通知恢复为止。condition_variable是一个类,这个类既有构造函数也有析构函数,使用时需要构造对应的condition_variable对象,调用对象相应的函数来实现上面的功能。
包含以下成员:

//等待
cv.wait(unique_lock);
cv.wait(unique_lock , [&a]{return a.s =="AAA";});//这里是还需要加个判断。如果为真,则启动。

// wait_for Wait for timeout or until notified
// wait_until Wait until notified or time point
//以上两个略了,就是等待一段时间和等待到一个时间点

cv.notify_one();
//解锁一个线程,如果有多个,则未知哪个线程执行
cv.notify_all(); 
//解锁所有线程
// cv_status 这是一个类,表示variable 的状态,如下所示
enum class cv_status { no_timeout, timeout };

示例和对比:(CPU  intel i5-13400)

线程一不断向string中增加字母A,线程2检测到string为9900个的A之后重置string为B。

用condition_variable情况

#include <iostream>  // std::cout
#include <thread>  // std::thread
#include <mutex>  // std::mutex
#include <condition_variable>

std::condition_variable cv;
class A{
 public:
  std::string s;
  std::mutex mtx1;
};

void print_block1 (int n, char c , A& a) {
  for (int i = 0; i < n; ++i) {
    {
      std::lock_guard<std::mutex> lock1(a.mtx1);
      a.s += c;
      std::cout << std::this_thread::get_id() << "," << a.s << std::endl;
    }
    //这样发送会有问题。可以减少到5,试一下,他会在A有6个时才重置为B。
    //if(a.s == std::string(19990,'A')) {
      cv.notify_one();
    //  std::this_thread::sleep_for(std::chrono::nanoseconds(1));
    //}
  }
}

void print_block2 (int n, char c, A& a) {
  //while (true) {
    std::unique_lock<std::mutex> lock1(a.mtx1);
    cv.wait(lock1 , [&a]{return a.s ==std::string(19990,'A');});//用这个
    //cv.wait(lock1);这个对应用if判断后notify的方法。
    //if (a.s == std::string(19990,'A')) {
      a.s = c;
      std::cout << std::this_thread::get_id() << "," << a.s << std::endl;
    //  break;
    //}
  //}
}

int main ()
{
  A a1;
  auto t1 = std::chrono::system_clock::now();
  std::thread th1 (print_block1,20000, 'A' , std::ref(a1));//线程1:打印*
  std::thread th2 (print_block2,5, 'B' , std::ref(a1));//线程2:打印$

    if(th1.joinable() && th2.joinable())
    {
        th1.join();
        th2.join();
    }
    auto t2 = std::chrono::system_clock::now();
    auto d = std::chrono::duration_cast<std::chrono::microseconds>(t2-t1);
    std::cout << "spend time:" << d.count() << std::endl;
    return 0;
}

循环判断的情况:线程2不断重复判断

#include <iostream>  // std::cout
#include <thread>  // std::thread
#include <mutex>  // std::mutex
#include <condition_variable>

std::condition_variable cv;
class A{
 public:
  std::string s;
  std::mutex mtx1;
};

void print_block1 (int n, char c , A& a) {
  for (int i = 0; i < n; ++i) {
    {
      std::lock_guard<std::mutex> lock1(a.mtx1);
      a.s += c;
      std::cout << std::this_thread::get_id() << "," << a.s << std::endl;
    }
  }
}

void print_block2 (int n, char c, A& a) {
  while (true) {
    std::lock_guard<std::mutex> lock1(a.mtx1);
    if (a.s == std::string(19990,'A')) {
      a.s = c;
      std::cout << std::this_thread::get_id() << "," << a.s << std::endl;
      break;
    }
  }
}

int main ()
{
  A a1;
  auto t1 = std::chrono::system_clock::now();
  std::thread th1 (print_block1,20000, 'A' , std::ref(a1));//线程1:打印*
  std::thread th2 (print_block2,5, 'B' , std::ref(a1));//线程2:打印$

    if(th1.joinable() && th2.joinable())
    {
        th1.join();
        th2.join();
    }
    auto t2 = std::chrono::system_clock::now();
    auto d = std::chrono::duration_cast<std::chrono::microseconds>(t2-t1);
    std::cout << "spend time:" << d.count() << std::endl;
    return 0;
}

 ​​​​​​​

CPU情况,前者使用条件变量,后者使用循环判断。感觉两者在CPU占用上差不多,时间上使用条件变量略快一点点。我以为线程2阻塞等待时会不占用cpu资源,更节省资源才对。

这里面遇到了一个问题:

1、线程1先判断满足条件后notify_one,线程2cv.wait(lck);这种情况下会导致线程2执行时慢一拍,导致输出错误,需要线程1notify_one以后等待1纳秒。但是这样就会导致用条件变量的时间变得波动很大,耗时很长。

2、线程1每次循环都notify_one,线程2去判断cv.wait(lock1 , [&a]{return a.s == std::string(5,'A');});  这种情况下比较正常和稳定,个人理解更推荐这样使用。

五、原子操作

下次更新

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++ std::Thread多线程和mutex锁通俗易懂 的相关文章

随机推荐

  • JVM性能优化 —— 类加载器,手动实现类的热加载

    一 类加载的机制的层次结构 每个编写的 java 拓展名类文件都存储着需要执行的程序逻辑 这些 java 文件经过Java编译器编译成拓展名为 class 的文件 class 文件中保存着Java代码经转换后的虚拟机指令 当需要使用某个类时
  • qt5.5程序打包发布以及依赖

    玩qt5也有一段时间了 惭愧的是一直没有好好的发布过程序 因为写的都是小程序没啥需要用到发布 而且qt也说不上很熟悉 本来打算到基本掌握qt之后再来研究研究怎么打包程序 最近晚上的空闲时间多了 闲着也是闲着 于是便来试试 在网上搜索了一下资
  • Newifi3(新路由3)刷潘多拉(Pandora)固件

    最近在淘宝入手了一个二手的newifi3 主要是因为它内存大 而且性价比相当高 512M的ddr2和32M的flash买下来才100左右 下面介绍如何刷 Pandora固件 步骤 1 找一根网线 一端插入路由器wan口 一端插入电脑 把电脑
  • Python爬虫从入门到精通:(11)数据解析_站长素材图片的爬取(爬取俄罗斯高清图片名称和地址)* _Python涛哥

    站长素材图片的爬取 爬取俄罗斯高清图片名称和地址 地址 https sc chinaz com tupian eluosi html 我们先来分析下页面 获取图片名称和地址定位 我们很容易获取到了图片名称和地址是在 div div div
  • SQL语句截取字段某指定字符的前半段/后半段内容

    最近项目中遇到一个小问题 需要从数据库中取出对应数据 并根据某个字段中的前半段内容进行排序 搜索资料后得以解决 现将解决方法记录如下 最初的查询SQL SELECT file name sort FROM base annexesfile
  • 盛最多水的容器

    给定一个长度为 n 的整数数组 height 有 n 条垂线 第 i 条线的两个端点是 i 0 和 i height i 找出其中的两条线 使得它们与 x 轴共同构成的容器可以容纳最多的水 返回容器可以储存的最大水量 说明 你不能倾斜容器
  • Palindrome subsequence【区间DP+冗斥】

    题目链接HDU 4632 题目让我们求给定的一段字符串上回文串的长度 一个数也算是回文串 于是我就想怎样去找其中的规律 我举了些例子 先是从相同的字符串开始举例 aaa 对于aaa dp 1 1 1 dp 2 2 1 dp 1 2 dp 1
  • 遥感+python 目录

    遥感 python 1 遥感影像预处理 环境搭建 辐射定标 大气校正 RPC校正 重投影 2 遥感影像样本读取 CSV 格式样本读取 XLS 格式样本读取 TIFF 格式样本读取 SHAPE 格式样本读取 3 遥感影像分类器构建 KNN分类
  • Linux——进程信号的发送

    目录 一 信号发送的概念 首先来讲几个发送术语 它有三种情况 注意 二 信号在内核中的表示示意图 三 信号捕捉 所以总结一下 此时 会出现这样一个疑问 操作系统是如何得知现在被执行的进程是用户态还是内核态 问题2 CPU在执行某个进程时是如
  • Node.js 应用:Koa2 使用 JWT 进行鉴权

    前言 在前后端分离的开发中 通过 Restful API 进行数据交互时 如果没有对 API 进行保护 那么别人就可以很容易地获取并调用这些 API 进行操作 那么服务器端要如何进行鉴权呢 Json Web Token 简称为 JWT 它定
  • React全部api解读

    很多同学用react开发的时候 真正用到的React的api少之又少 基本停留在Component React memo等层面 实际react源码中 暴露出来的方法并不少 只是我们平时很少用 但是React暴露出这么多api并非没有用 想要
  • pip(Python包管理工具)安装第三方库教程

    目录 1 python环境检查 2 pip库的常用命令 2 1 更新包 2 1 1 更新pip工具 2 1 2 更新三方库 2 2 安装包 2 2 1 在线安装 2 2 1 1 直接安装 2 2 1 2 镜像安装 2 2 2 离线安装 2
  • 机器学习概述和数据预处理

    概述 机器学习定义 机器学习是一门能够让编程计算机从数据中学习的计算机科学 一个计算机程序在完成任务T之后 获得经验E 其表现效果为P 如果任务T的性能表现 也就是用来衡量的P 随着E增加而增加 那么这样计算机程序就被称为机器学习系统 自我
  • Vue 2项目如何升级到Vue 3?

    应不应该从 Vue 2 升级到 Vue 3 应不应该升级 这个问题不能一概而论 首先 如果你要开启一个新项目 那直接使用 Vue 3 是最佳选择 后面课程里 我也会带你使用 Vue 3 的新特性和新语法开发一个项目 以前我独立使用 Vue
  • Linux磁盘管理命令

    Linux磁盘管理命令 Linux磁盘管理命令 1 pwd命令 2 cd命令 3 df命令 4 mkdir命令 5 mount及umount命令 6 ls命令 7 history命令 Linux磁盘管理命令 1 pwd命令 作用 显示当前工
  • ubuntu18.04安装Azure Kinect传感器摄像头教程

    官方教程 Azure Kinect DK文档 配置存储库 可以通过安装适用于 Linux 分发版和版本的 Linux 包自动配置存储库 此包将安装存储库配置以及工具 如 apt yum zypper 使用的 GPG 公钥来验证已签名的包和
  • 基于STM32CubeMX创建的STM32H743+DP83848+LWIP网络通信程序调试_20221127算是胎教级教程了

    目录 目的 编写一个可以稳定连接到局域网的STM32网络通信程序 硬件和软件 具体步骤 1 利用STM32CubeMX建立Keil工程文件 2 在keil中修改代码和配置工程 3 代码烧录 功能验证 目的 编写一个可以稳定连接到局域网的ST
  • 关于xpath的安装

    1 xpath简介 使用xpath需要安装模块 pip install lxml 导入模块 from lxml import etree xpath是用来载xml中查找指定的元素 它是一种路径表达式 详细内容可在文档中查找 https de
  • linux中软链接的使用方法

    在 Linux 中的连结有两种 一种是类似 Windows 的快捷方式功能的档案 可以让你快速的链接到目标档案 或目彔 另一种则是透过文件系统的 inode 连结来产生新的文档名 而不是产生新档案 这种称为实体链接 hard link Ha
  • C++ std::Thread多线程和mutex锁通俗易懂

    记录学习过程 如有新的发现 随时补充 如有错误或补充 请各位大佬指正 一 前言 多线程有多种方式 std Thread boost Thread pthread Windows库等 本文只关注std Thread 可以跨平台运行 二 std