C++ 多线程笔记2 线程同步

并发(Concurrency)和并行(Parallelism)

并发是指在单核CPU上,通过时间片轮转的方式,让多个任务看起来像是同时进行的。实际上,CPU在一个时间段内只会处理一个任务,但是由于切换时间非常快,用户感觉像是多个任务同时在进行。

这种方式的优点是可以充分利用CPU资源,提高系统的响应能力。然而,由于CPU需要频繁地切换任务,这会带来上下文切换的开销,可能会导致系统效率下降。

并行处理是指多核CPU在同一时刻同时处理多个任务。每个核心都有自己的独立寄存器和运算单元,可以独立地执行任务。这种方式的优点是可以显著提高系统的处理能力,因为多个任务可以真正的同时进行。

然而,并行处理也有其缺点。首先,不是所有的任务都可以并行化,有些任务可能更适合串行执行。其次,并行处理需要更多的硬件资源,如内存和总线带宽,这可能会增加系统的成本。

IO密集型程序和CPU密集型程序

IO密集型程序是那些在执行过程中大部分时间都花费在输入/输出操作上的程序,如文件读写、网络通信等。

CPU密集型程序指的是那些在执行过程中大部分时间都用于计算操作,如数学计算、逻辑运算、数据处理等。

因此IO密集型程序适合采用多线程的并行机制提高性能,而CPU密集型不一定,因为线程的上下文切换太过于耗费CPU时间,所以不是多线程就代表高性能程序。

当如果是多CPU多核的情况下,CPU密集型程序也适合采用多线程执行,充分利用性能。

多线程的线程数量怎么确定

为了完成任务,线程真的越多越好吗?

  • 线程的创建和销毁都是“重操作”,需要与操作系统内核空间进行交互,是相对昂贵的操作。

    在服务执行的过程去实时创建销毁线程。

  • 线程栈本身也会占用大量内存。每一个线程都需要线程栈,栈都被占完了无法做事情。

  • 线程上下文切换要占用大量时间,上下文切换花费的CPU时间也特别多,导致CPU利用率就不高了。

  • 大量线程唤醒会使得系统出现锯齿状负载或者瞬时负载导致宕机

一般会根据CPU的核心数量来确定线程。

线程池的优势

操作系统上创建线程和销毁线程都是很“重“的操作,耗时耗性能都比较多,那么在服务执行的过程中,如果业务量比较大,实时的去创

建线程、执行业务、业务完成后销毁线程,那么会号致系统的实时性能降低,业务的处理能力也会降低。

线程池的优势就是(每个池都有自己的优势),在服务进程启动之初,就事先创建好线程池里面的线程,当业务流量到来时需要分配线

程,直接从线程池中获取一个空闲线程执行tsk任务即可,task执行完成后,也不用释放线程,而是把线程归还到线程池中继续给后续

的task提供服务。

ixed模式线程池

线程池里面的线程个数是固定不变的,一般是ThreadPoolf创建时根据当前机器的CPU核心数量进行指定。

cached模式线程池

线程池里面的线程个数是可动态增长的,根据任务的数量动态的增加线程的数量,但是会设置一个线程数量的阈值(线程过多的坏处上

面已经讲过了),任务处理完成,如果动态增长的线程空闲了60s还没有处理其它任务,那么关闭线程,保持池中最初数量的线程即可。

线程间的互斥

如果两个及多个线程访问同一个资源,根据CPU的调度,可能会出现不同的结果,为了让同一个时刻只有一个线程能访问资源,我们首先看看C++中的mutex。

传统的互斥锁 mutex

  1. #include <iostream>
  2. #include <thread>
  3. #include <mutex>
  4. std::mutex mtx; // 全局互斥量
  5. void print_block(int n, char c) {
  6. mtx.lock(); // 请求互斥量
  7. for (int i = 0; i < n; ++i) {
  8. std::cout << c;
  9. }
  10. std::cout << '\n';
  11. mtx.unlock(); // 释放互斥量
  12. }
  13. int main() {
  14. std::thread th1(print_block, 50, '*');
  15. std::thread th2(print_block, 50, '$');
  16. th1.join();
  17. th2.join();
  18. return 0;
  19. }

这样虽然可以保证同一时刻只有一个线程可以访问,但是

**mtx.lock(); // 请求互斥量 **

...

** mtx.unlock(); // 释放互斥量 **

如果中间发生了异常,unlock()就无法进行调用,就会陷入死锁机制。

C++中的对象lock_guard

std::lock_guard<std::mutex> lock(mtx);

  1. std::lock_guard C++11 引入的一个类模板,用于简化互斥锁(mutex)的管理。它提供了一种自动锁定和解锁互斥锁的机制,从而减少了由于忘记解锁或异常导致的死锁风险。
  2. std::lock_guard 的使用非常直接。当你创建一个 std::lock_guard 对象时,它会尝试锁定关联的互斥锁。当 std::lock_guard 对象离开其作用域或被销毁时,它会自动解锁关联的互斥锁。

代码介绍:


  1. #include <thread>
  2. #include <atomic>
  3. #include <iostream>
  4. #include <list>
  5. #include <mutex>
  6. int ticketCount = 100;
  7. std::mutex mtx; //创建全局互斥锁
  8. void service(int index)
  9. {
  10. while (ticketCount > 0)
  11. {
  12. {
  13. std::lock_guard<std::mutex> lock(mtx); //使用互斥锁
  14. if (ticketCount > 0)
  15. {
  16. std::cout << "第" << index << "线程,卖出" << ticketCount << "张票\n";
  17. ticketCount--;
  18. }
  19. }//出了这个作用域就会调用析构函数
  20. std::this_thread::sleep_for(std::chrono::milliseconds(100));
  21. }
  22. }
  23. int main()
  24. {
  25. std::list<std::thread> tlist;
  26. for (int i = 1; i <= 10; ++i)
  27. tlist.push_back(std::thread(service, i));
  28. for (auto& t : tlist)
  29. t.join();
  30. }

线程之间的通信

std::lock_guardstd::mutex 虽然用起来很方便,但是无法解决线程通信的问题

线程通信

是指在多线程编程中,不同的线程之间需要进行信息交换同步协作的过程。由于每个线程都有自己的执行栈和局部变量,它们不能直接访问其他线程的内存空间,因此需要通过一些机制来实现线程之间的通信。

线程通信的主要目的包括:

  1. 数据共享:多个线程可能需要访问和修改共享的数据结构或资源。线程通信机制确保这些操作能够正确同步,避免数据竞争和不一致。

  2. 同步协作:线程之间可能需要按照一定的顺序执行操作,或者等待其他线程完成某个任务后再继续执行。同步机制(如互斥锁、条件变量、信号量等)可以帮助实现这种协作。

  3. 消息传递:一个线程可能需要向另一个线程发送消息或信号,以通知它进行某种操作或响应某个事件。消息队列、管道、套接字等机制可以用于线程间的消息传递。

  4. 任务划分与合并:线程可以将任务划分为更小的子任务,并在不同的线程上并行执行。完成后,这些线程需要合并结果或进行后续操作。

线程通信是多线程编程中的一个重要概念,它对于确保程序的正确性和性能至关重要。正确的线程通信可以避免竞态条件、死锁和其他并发问题,从而实现高效、可靠的并发执行。

举个例子:比如说经典的生产者消费者问题,在同一个资源中 ,如果该资源为空,生产者模块就会生成新的,当资源>0,消费者模块就会消费掉一个资源。

而不正确的线程通信中,资源为空的时候去一直消费,或者资源>0还在一直生产,就会导致死锁问题。

这个时候我们使用C++的新对象来解决问题,先看看代码:

  1. #include <thread>
  2. #include <atomic>
  3. #include <iostream>
  4. #include <list>
  5. #include <mutex>
  6. #include <queue>
  7. #include <condition_variable>
  8. std::mutex mtx;
  9. std::condition_variable cv;
  10. class Quque {
  11. public:
  12. void put(int val)
  13. {
  14. //std::lock_guard<std::mutex> lckg(mtx);
  15. std::unique_lock<std::mutex> ulckg(mtx);
  16. while (!que.empty()) //不为空就停止,等取出了再继续
  17. {
  18. cv.wait(ulckg);
  19. }
  20. que.push(val);
  21. std::cout << "生产者 生产:" << val << "号物品" << std::endl;
  22. cv.notify_all();
  23. }
  24. int get()
  25. {
  26. //std::lock_guard<std::mutex> lckg(mtx);
  27. std::unique_lock<std::mutex> ulckg(mtx);
  28. while (que.empty())
  29. cv.wait(ulckg);
  30. int val = que.front();
  31. que.pop();
  32. cv.notify_all();
  33. std::cout << "生产者 消费:" << val << "号物品" << std::endl;
  34. return val;
  35. }
  36. private:
  37. std::queue<int> que;
  38. };
  39. //生产者
  40. void producer(Quque* que)
  41. {
  42. for (int i = 1; i < 11; ++i)
  43. {
  44. que->put(i);
  45. std::this_thread::sleep_for(std::chrono::milliseconds(100));
  46. }
  47. }
  48. //消费者
  49. void consumer(Quque* que)
  50. {
  51. for (int i = 1; i < 11; ++i)
  52. {
  53. que->get();
  54. std::this_thread::sleep_for(std::chrono::milliseconds(100));
  55. }
  56. }
  57. int main()
  58. {
  59. Quque q;
  60. std::thread t1(producer, &q);
  61. std::thread t2(consumer, &q);
  62. t1.join();
  63. t2.join();
  64. return 0;
  65. }

输出内容

  1. 生产者 生产:1号物品
  2. 生产者 消费:1号物品
  3. 生产者 生产:2号物品
  4. 生产者 消费:2号物品
  5. 生产者 生产:3号物品
  6. 生产者 消费:3号物品
  7. 生产者 生产:4号物品
  8. 生产者 消费:4号物品
  9. 生产者 生产:5号物品
  10. 生产者 消费:5号物品
  11. 生产者 生产:6号物品
  12. 生产者 消费:6号物品
  13. 生产者 生产:7号物品
  14. 生产者 消费:7号物品
  15. 生产者 生产:8号物品
  16. 生产者 消费:8号物品
  17. 生产者 生产:9号物品
  18. 生产者 消费:9号物品
  19. 生产者 生产:10号物品
  20. 生产者 消费:10号物品

首先是里面的std::unique_lockstd::mutex ulckg(mtx); 和std::condition_variable cv;

std::condition_variable 是 C++11 引入的一个类,用于支持线程间的条件同步。它常常与互斥锁(std::mutex)一起使用,以实现一个或多个线程等待某个条件成立,而另一个线程在条件成立时通知等待的线程。

以下是 std::condition_variable 的主要作用和使用场景:

  1. 等待条件成立

    线程可以使用 std::condition_variablewait() 方法进入等待状态,直到另一个线程通过 notify_one()notify_all() 方法发出通知。wait() 方法会自动解锁关联的互斥锁,使等待的线程能够进入睡眠状态。当通知到来时,wait() 会重新锁定互斥锁并返回,这样线程可以检查条件是否已满足。

  2. 通知等待线程

    当某个条件满足时(例如,某个共享资源已经准备好或被修改),一个线程可以使用 notify_one()notify_all() 方法来唤醒一个或所有等待在 std::condition_variable 上的线程。

  3. 线程间协作

    std::condition_variable 常常用于生产者-消费者问题、多线程任务队列、线程池管理等场景中,以实现线程间的协作和同步。

  4. 避免虚假唤醒

    由于操作系统调度的原因,线程可能会被“虚假唤醒”(即在没有收到通知的情况下醒来)。std::condition_variablewait() 方法考虑到了这一点,因此通常与互斥锁和条件检查一起使用,以确保线程在继续执行前确实收到了通知,并且条件已经满足。

而在上面代码中cv.wait(ulckg);表示了在条件下进入等待状态,除非收到cv.notify_all();并且mutex已经被unlock,才会继续进行线程运行。

std::unique_lock相比std::lock_guard提供了更多的灵活性,因为它允许延迟锁定手动控制锁定解锁、条件等待以及所有权转移。这使得std::unique_lock在需要更精细控制锁定时非常有用,比如在需要响应中断异常处理时。

此外,std::unique_lock还可以与std::defer_lockstd::try_to_lockstd::adopt_lock标签配合使用,以在构造时指定不同的锁定行为。例如,使用std::defer_lock标签可以在构造时不锁定互斥锁,稍后再通过调用lock()方法来锁定。

C++ 多线程笔记2 线程同步的更多相关文章

  1. .NET面试题解析(07)-多线程编程与线程同步

      系列文章目录地址: .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引 关于线程的知识点其实是很多的,比如多线程编程.线程上下文.异步编程.线程同步构造.GUI的跨线程访问等等, ...

  2. java核心知识点学习----多线程并发之线程同步

    1.什么是线程同步? 多线程编程是很有趣的事情,它很容易出现"错误情况",这种情况不是由编码造成的,它是由系统的线程调度造成的,当使用多个线程来访问同一个数据时,很容易出现&quo ...

  3. .NET面试题解析(07)-多线程编程与线程同步 (转)

    http://www.cnblogs.com/anding/p/5301754.html 系列文章目录地址: .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引 关于线程的知识点其实 ...

  4. java笔记--关于线程同步(7种同步方式)

    关于线程同步(7种方式) --如果朋友您想转载本文章请注明转载地址"http://www.cnblogs.com/XHJT/p/3897440.html"谢谢-- 为何要使用同步? ...

  5. 细说C#多线程那些事 - 线程同步和多线程优先级

    上个文章分享了一些多线程的一些基础的知识,今天我们继续学习. 一.Task类 上次我们说了线程池,线程池的QueueUserWorkItem()方法发起一次异步的线程执行很简单 但是该方法最大的问题是 ...

  6. java笔记--关于线程同步(5种同步方式)【转】

    为何要使用同步?     java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),      将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完 ...

  7. Win32多线程编程(3) — 线程同步与通信

      一.线程间数据通信 系统从进程的地址空间中分配内存给线程栈使用.新线程与创建它的线程在相同的进程上下文中运行.因此,新线程可以访问进程内核对象的所有句柄.进程中的所有内存以及同一个进程中其他所有线 ...

  8. C# 多线程编程第二步——线程同步与线程安全

    上一篇博客学习了如何简单的使用多线程.其实普通的多线程确实很简单,但是一个安全的高效的多线程却不那么简单.所以很多时候不正确的使用多线程反倒会影响程序的性能. 下面先看一个例子 : class Pro ...

  9. java多线程二之线程同步的三种方法

          java多线程的难点是在:处理多个线程同步与并发运行时线程间的通信问题.java在处理线程同步时,常用方法有: 1.synchronized关键字. 2.Lock显示加锁. 3.信号量Se ...

  10. java笔记--关于线程同步(5种同步方式)

    转自:http://www.2cto.com/kf/201408/324061.html 为何要使用同步?     java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改 ...

随机推荐

  1. 关于Js debounce(防抖)函数和throttle(节流)小结

    闭包的实际运用防抖 防抖:当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次, 如果设定的时间到来之前,又一次触发了事件,就重新开始 延时. (如果在一段时间内,又触发了该事件:就 ...

  2. 同步存储读取vuex中store中的值

    main.js import store from "./store"; Vue.prototype.$store = store; 在 store中的index.js中 impo ...

  3. Unity的SpriteAtlas实践

    我的环境 Unity引擎版本:Unity2019.3.7f1 AssetBundles-Browser 于2021-1-14拉取,github上最后提交日期是2019-12-14,在本文简称:ABBr ...

  4. 《Mastering ABP Framework》图书目录

    以下是<Mastering ABP Framework>的中文目录,目前个人正在进行翻译中,如果您对本书感兴趣,也想使用或者学习框架设计,可以访问该地址进行登记,本文只是一个目的的展示和购 ...

  5. KB0004.如何进行DoraCloud版本升级?

    升级过程为: 1).现有版本,进入维护模式,导出系统数据.    2).记录现当前版本DoraCloud VM 的IP地址,子网掩码.网关.DNS信息,将VM关机. 3).安装新版本DoraCloud ...

  6. 数学微积分,学习笔记,等价无穷小的证明:(1+x)^a-1 ~ ax

    \(\lim_{x \to 0} \frac{\sqrt[n]{1+x} -1}{\frac{x}{n} } =1\)的证明 \[\lim_{x \to 0} \frac{\sqrt[n]{1+x} ...

  7. git常用命令(企业级)

    一: 常用git命令 # 初始化,将已有的文件初始化为git仓库 git init # 查询文件状态[绿色暂存区,红色表示工作区更改了,没有提交到暂存区] git status git status ...

  8. 在QEMU-KVM环境下部署Oracle 19.16 RAC

    KVM环境和其他虚拟化或真实生产最大差异主要就是在实施前期准备工作上: 具体在 DB节点 和存储环境 的准备工作上有差异,本文会详细说明. 而剩余基本软件安装和补丁应用部分无差异,若不清楚可以直接参考 ...

  9. 使用了未经检查或不安全的操作。 有关详细信息, 请使用 -Xlint:unchecked 重新编译

  10. linux-Shell将命令行终端输出结果写入保存到文件中

    (一)将输出与错误写到同一个文件(1)方法1 #!bin/bashjava -jar hbase-example.jar 2>&1 | tee hbase_log.txt 说明:0,1, ...