C++进阶 多线程相关(下)

简介: C++进阶 多线程相关(下)

mutex库

不加锁会出现的问题

我们写出下面的代码 : 创建两个线程 让这两个线程执行同一个任务 打印0~99的数字

void func(int n)
{
  for (int i = 0; i < n; i++)
  {
    cout << this_thread::get_id() << " : " << i << endl;
  }
}
int main()
{
  thread t1 = thread(func, 100);
  thread t2 = thread(func, 100);
  t1.join();
  t2.join();
  return 0;
}

我们会发现打印的时候出现这种情况

2580a9d7532e4c229ff8048104d99706.png

这是因为我们没有给线程加锁 在一个线程运行到一半的时候时间到了 切换到另一个线程的输出

解决的方式就是通过加锁

我们首先在定义一个锁 mtx

mutex mtx;

并且在任务开始之前加锁 任务开始之后解锁

mtx.lock();
  for (int i = 0; i < n; i++)
  {
    cout << this_thread::get_id() << " : " << i << endl;
  }
  mtx.unlock();

线程运行就不会出现上面的问题了

b8bdcb9b459340ffbe5a97d108563af1.png

C++线程库的引用问题

在C++的线程库当中 我们不能直接使用引用传递参数 否则一些编译器会报错 (博主使用的vs2022) 一些编译器虽然编译通过了 但是没有引用的效果 (我们老师使用vs2019出现上述效果)

如果说我们要往线程调用函数中传引用 则我们需要使用到这样的一个函数

ref(value)

代码和表示结果如下

void func(int n , int& x)
{
  for (int i = 0; i < n; i++)
  {
    mtx.lock();
    cout << this_thread::get_id() << " : " << i << endl;
    x++;
    mtx.unlock();
  }
}
int main()
{
  int x = 0;
  thread t1 = thread(func, 10,  ref(x));
  thread t2 = thread(func, 10,  ref(x));
  t1.join();
  t2.join();
  cout << x << endl;
  return 0;
}

02240b8cf9e54d71a6e63488b5aa55a8.png

当然 我们使用全局锁是很不安全的 并且会污染命名空间 所以说最好我们把锁也定义在main函数当中 并且以参数的形式 引用传递给函数

注意!我们不能使用传值的形式传递锁 因为它的拷贝构造函数被禁用了

演示结果如下

eaa36485c49e4a499dd53d73fdb2036a.png

函数指针替换

我们在线程传参的时候不光可以传函数指针 还可以传递仿函数和lamabda表达式(底层就是仿函数)等等

有关于lamadba表达式的内容大家可以参考我的这篇博客

lamabda表达式

原子操作相关

关于原子性的相关问题 我在Linux线程互斥中详细介绍了 大家可以参考我的这篇博客

Linux线程互斥

如果大家阅读完了上面一篇博客之后就会知道 x++; 这并不是一个原子操作

要保证我们一个操作是原子的 除了加锁之外我们还可以使用C++提供的一个原子类

它的类名叫做 atomic

我们可以这么定义一个变量

atomic<int> x

此时我们使用 x++ 就是一个原子操作了 也就不需要互斥锁了

为什么我们使用atomic之后x++就变成原子操作了呢?

这里其实和我们mysql当中的事务很类似

具体可以参考我的这篇博客

mysql事务

它将x++底层的三条汇编语言封装成了一个整体 要么成功 要么失败 (失败之后就回滚)

靠着事务保证了原子性

注意:

  • 由于我们要保证原子性操作的资源一般是临界资源 所以说是不允许拷贝的 所以说atomic类中禁用了拷贝构造 移动构造等函数

条件变量库

学习到这个阶段我们应该有了一定自主学习的能力了

我们这里只介绍几个常用的函数 如果大家想深入学习以后可以直接在cspp网站上搜索函数学习

现在要求我们实现两个线程交替打印1-100

尝试用两个线程交替打印1-100的数字,要求一个线程打印奇数,另一个线程打印偶数,并且打印数字从小到大依次递增。

我们尝试使用上面学过的知识去做这道题

代码和表示结果如下

void func(mutex& mtx)
{
  for (int i = 0; i < 100; i+=2)
  {
    mtx.lock();
    cout << this_thread::get_id() << " : " << i << endl;
    mtx.unlock();
  }
}
void func2(mutex& mtx)
{
  for (int i = 1; i < 100; i += 2)
  {
    mtx.lock();
    cout << this_thread::get_id() << " : " << i << endl;
    mtx.unlock();
  }
}
int main()
{
  mutex mtx;
  thread t1 = thread(func ,ref(mtx));
  thread t2 = thread(func2, ref(mtx));
  t1.join();
  t2.join();
  return 0;
}

a7e2fedbf6944a66b87e286e49cbdc13.png

我们发现 虽然我们能够让两个线程分别打印奇数和偶数 但是却不能让它们依次执行

在解决了原子性之后就该我们的条件变量库上场了

我们一般会这样子定义一个条件变量

condition_variable cv;

然后我们用两个函数来使用它

template <class Predicate>
  void wait (unique_lock<mutex>& lck, Predicate pred);

参数说明:

  • 第一个参数是一个互斥锁 我们使用之前定义的锁构造一个就可以
  • 第二个参数是一个函数指针 它返回一个bool类型的参数 可以被反复调用 直到返回true为止

在线程进入等待状态之后会主动释放锁

void notify_one() noexcept;

我们可以使用该函数来唤醒处于等待状态的一个线程 唤醒处于等待状态的线程之后会自动获取锁

那么使用上面学的条件变量我们就可以修改我们的代码 使它完成功能

condition_variable cv;
bool flag = true;
bool Flag()
{
  return flag;
}
bool UFlag()
{
  return !flag;
}
void func(mutex& mtx)
{
  unique_lock<mutex> lck(mtx);
  for (int i = 0; i < 100; i+=2)
  {
    cv.wait(lck, Flag);
    cout << this_thread::get_id() << " : " << i << endl;
    flag = false;
    cv.notify_one();
  }
}
void func2(mutex& mtx)
{
  unique_lock<mutex> lck(mtx);
  for (int i = 1; i < 100; i += 2)
  {
    cv.wait(lck, UFlag);
    cout << this_thread::get_id() << " : " << i << endl;
    flag = true;
    cv.notify_one();
  }
}
int main()
{
  mutex mtx;
  thread t1 = thread(func ,ref(mtx));
  thread t2 = thread(func2, ref(mtx));
  t1.join();
  t2.join();
  return 0;
}


解释下上面的代码

  • 首先我们创造一个unique_lock 对象后它会自动调用lock()函数加锁
  • 当我们调用wait()函数的时候会自动解锁
  • 当我们调用notify_one()函数的时候会自动加锁

此外我们可以通过控制flag的初始值来控制哪一个线程先行动

运行结果如下

5bea73ae3bac4ceb83be3225f0467b7a.png

总结

63bcc9e9facd43af8f8f5378df8e3d1e.png

相关文章
|
1月前
|
数据可视化 关系型数据库 编译器
【C/C++ 单线程性能分析工具 Gprof】 GNU的C/C++ 性能分析工具 Gprof 使用全面指南
【C/C++ 单线程性能分析工具 Gprof】 GNU的C/C++ 性能分析工具 Gprof 使用全面指南
114 2
|
1月前
|
存储 算法 Java
【C/C++ 线程池设计思路】 深入探索线程池设计:任务历史记录的高效管理策略
【C/C++ 线程池设计思路】 深入探索线程池设计:任务历史记录的高效管理策略
74 0
|
1月前
|
消息中间件 Linux 调度
【Linux 进程/线程状态 】深入理解Linux C++中的进程/线程状态:阻塞,休眠,僵死
【Linux 进程/线程状态 】深入理解Linux C++中的进程/线程状态:阻塞,休眠,僵死
68 0
|
4天前
|
设计模式 C语言 C++
【C++进阶(六)】STL大法--栈和队列深度剖析&优先级队列&适配器原理
【C++进阶(六)】STL大法--栈和队列深度剖析&优先级队列&适配器原理
|
4天前
|
存储 缓存 编译器
【C++进阶(五)】STL大法--list模拟实现以及list和vector的对比
【C++进阶(五)】STL大法--list模拟实现以及list和vector的对比
|
4天前
|
算法 C++ 容器
【C++进阶(四)】STL大法--list深度剖析&list迭代器问题探讨
【C++进阶(四)】STL大法--list深度剖析&list迭代器问题探讨
|
4天前
|
编译器 C++
【C++进阶(三)】STL大法--vector迭代器失效&深浅拷贝问题剖析
【C++进阶(三)】STL大法--vector迭代器失效&深浅拷贝问题剖析
|
1月前
|
安全 Java 调度
【C/C++ 线程池设计思路 】设计与实现支持优先级任务的C++线程池 简要介绍
【C/C++ 线程池设计思路 】设计与实现支持优先级任务的C++线程池 简要介绍
45 2
|
1月前
|
Linux API C++
【C++ 线程包裹类设计】跨平台C++线程包装类:属性设置与平台差异的全面探讨
【C++ 线程包裹类设计】跨平台C++线程包装类:属性设置与平台差异的全面探讨
51 2
|
1月前
|
设计模式 安全 C++
【C++ const 函数 的使用】C++ 中 const 成员函数与线程安全性:原理、案例与最佳实践
【C++ const 函数 的使用】C++ 中 const 成员函数与线程安全性:原理、案例与最佳实践
71 2