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

相关文章
|
16天前
|
缓存 安全 C++
C++无锁队列:解锁多线程编程新境界
【10月更文挑战第27天】
30 7
|
16天前
|
消息中间件 存储 安全
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
43 1
C++ 多线程之初识多线程
|
22天前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
|
1月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
45 6
|
1月前
|
缓存 负载均衡 Java
c++写高性能的任务流线程池(万字详解!)
本文介绍了一种高性能的任务流线程池设计,涵盖多种优化机制。首先介绍了Work Steal机制,通过任务偷窃提高资源利用率。接着讨论了优先级任务,使不同优先级的任务得到合理调度。然后提出了缓存机制,通过环形缓存队列提升程序负载能力。Local Thread机制则通过预先创建线程减少创建和销毁线程的开销。Lock Free机制进一步减少了锁的竞争。容量动态调整机制根据任务负载动态调整线程数量。批量处理机制提高了任务处理效率。此外,还介绍了负载均衡、避免等待、预测优化、减少复制等策略。最后,任务组的设计便于管理和复用多任务。整体设计旨在提升线程池的性能和稳定性。
76 5
|
1月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
23 0
C++ 多线程之线程管理函数
|
1月前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
29 0
Linux C/C++之线程基础
|
3月前
|
Java 调度
基于C++11的线程池
基于C++11的线程池
|
4月前
|
算法 编译器 C++
开发与运维线程问题之在C++的原子操作中memory_order如何解决
开发与运维线程问题之在C++的原子操作中memory_order如何解决
43 2