C++ 多线程笔记2 线程同步
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
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 全局互斥量
void print_block(int n, char c) {
mtx.lock(); // 请求互斥量
for (int i = 0; i < n; ++i) {
std::cout << c;
}
std::cout << '\n';
mtx.unlock(); // 释放互斥量
}
int main() {
std::thread th1(print_block, 50, '*');
std::thread th2(print_block, 50, '$');
th1.join();
th2.join();
return 0;
}
这样虽然可以保证同一时刻只有一个线程可以访问,但是
**mtx.lock(); // 请求互斥量 **
...
** mtx.unlock(); // 释放互斥量 **
如果中间发生了异常,unlock()就无法进行调用,就会陷入死锁机制。
C++中的对象lock_guard
std::lock_guard<std::mutex> lock(mtx);
std::lock_guard 是 C++11 引入的一个类模板,用于简化互斥锁(mutex)的管理。它提供了一种自动锁定和解锁互斥锁的机制,从而减少了由于忘记解锁或异常导致的死锁风险。
std::lock_guard 的使用非常直接。当你创建一个 std::lock_guard 对象时,它会尝试锁定关联的互斥锁。当 std::lock_guard 对象离开其作用域或被销毁时,它会自动解锁关联的互斥锁。
代码介绍:
#include <thread>
#include <atomic>
#include <iostream>
#include <list>
#include <mutex>
int ticketCount = 100;
std::mutex mtx; //创建全局互斥锁
void service(int index)
{
while (ticketCount > 0)
{
{
std::lock_guard<std::mutex> lock(mtx); //使用互斥锁
if (ticketCount > 0)
{
std::cout << "第" << index << "线程,卖出" << ticketCount << "张票\n";
ticketCount--;
}
}//出了这个作用域就会调用析构函数
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
std::list<std::thread> tlist;
for (int i = 1; i <= 10; ++i)
tlist.push_back(std::thread(service, i));
for (auto& t : tlist)
t.join();
}
线程之间的通信
std::lock_guardstd::mutex 虽然用起来很方便,但是无法解决线程通信的问题。
线程通信
是指在多线程编程中,不同的线程之间需要进行信息交换或同步协作的过程。由于每个线程都有自己的执行栈和局部变量,它们不能直接访问其他线程的内存空间,因此需要通过一些机制来实现线程之间的通信。
线程通信的主要目的包括:
数据共享:多个线程可能需要访问和修改共享的数据结构或资源。线程通信机制确保这些操作能够正确同步,避免数据竞争和不一致。
同步协作:线程之间可能需要按照一定的顺序执行操作,或者等待其他线程完成某个任务后再继续执行。同步机制(如互斥锁、条件变量、信号量等)可以帮助实现这种协作。
消息传递:一个线程可能需要向另一个线程发送消息或信号,以通知它进行某种操作或响应某个事件。消息队列、管道、套接字等机制可以用于线程间的消息传递。
任务划分与合并:线程可以将任务划分为更小的子任务,并在不同的线程上并行执行。完成后,这些线程需要合并结果或进行后续操作。
线程通信是多线程编程中的一个重要概念,它对于确保程序的正确性和性能至关重要。正确的线程通信可以避免竞态条件、死锁和其他并发问题,从而实现高效、可靠的并发执行。
举个例子:比如说经典的生产者消费者问题,在同一个资源中 ,如果该资源为空,生产者模块就会生成新的,当资源>0,消费者模块就会消费掉一个资源。
而不正确的线程通信中,资源为空的时候去一直消费,或者资源>0还在一直生产,就会导致死锁问题。
这个时候我们使用C++的新对象来解决问题,先看看代码:
#include <thread>
#include <atomic>
#include <iostream>
#include <list>
#include <mutex>
#include <queue>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
class Quque {
public:
void put(int val)
{
//std::lock_guard<std::mutex> lckg(mtx);
std::unique_lock<std::mutex> ulckg(mtx);
while (!que.empty()) //不为空就停止,等取出了再继续
{
cv.wait(ulckg);
}
que.push(val);
std::cout << "生产者 生产:" << val << "号物品" << std::endl;
cv.notify_all();
}
int get()
{
//std::lock_guard<std::mutex> lckg(mtx);
std::unique_lock<std::mutex> ulckg(mtx);
while (que.empty())
cv.wait(ulckg);
int val = que.front();
que.pop();
cv.notify_all();
std::cout << "生产者 消费:" << val << "号物品" << std::endl;
return val;
}
private:
std::queue<int> que;
};
//生产者
void producer(Quque* que)
{
for (int i = 1; i < 11; ++i)
{
que->put(i);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
//消费者
void consumer(Quque* que)
{
for (int i = 1; i < 11; ++i)
{
que->get();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
Quque q;
std::thread t1(producer, &q);
std::thread t2(consumer, &q);
t1.join();
t2.join();
return 0;
}
输出内容
生产者 生产:1号物品
生产者 消费:1号物品
生产者 生产:2号物品
生产者 消费:2号物品
生产者 生产:3号物品
生产者 消费:3号物品
生产者 生产:4号物品
生产者 消费:4号物品
生产者 生产:5号物品
生产者 消费:5号物品
生产者 生产:6号物品
生产者 消费:6号物品
生产者 生产:7号物品
生产者 消费:7号物品
生产者 生产:8号物品
生产者 消费:8号物品
生产者 生产:9号物品
生产者 消费:9号物品
生产者 生产:10号物品
生产者 消费:10号物品
首先是里面的std::unique_lockstd::mutex ulckg(mtx); 和std::condition_variable cv;
std::condition_variable 是 C++11 引入的一个类,用于支持线程间的条件同步。它常常与互斥锁(std::mutex)一起使用,以实现一个或多个线程等待某个条件成立,而另一个线程在条件成立时通知等待的线程。
以下是 std::condition_variable 的主要作用和使用场景:
等待条件成立:
线程可以使用std::condition_variable的wait()方法进入等待状态,直到另一个线程通过notify_one()或notify_all()方法发出通知。wait()方法会自动解锁关联的互斥锁,使等待的线程能够进入睡眠状态。当通知到来时,wait()会重新锁定互斥锁并返回,这样线程可以检查条件是否已满足。通知等待线程:
当某个条件满足时(例如,某个共享资源已经准备好或被修改),一个线程可以使用notify_one()或notify_all()方法来唤醒一个或所有等待在std::condition_variable上的线程。线程间协作:
std::condition_variable常常用于生产者-消费者问题、多线程任务队列、线程池管理等场景中,以实现线程间的协作和同步。避免虚假唤醒:
由于操作系统调度的原因,线程可能会被“虚假唤醒”(即在没有收到通知的情况下醒来)。std::condition_variable的wait()方法考虑到了这一点,因此通常与互斥锁和条件检查一起使用,以确保线程在继续执行前确实收到了通知,并且条件已经满足。
而在上面代码中cv.wait(ulckg);表示了在条件下进入等待状态,除非收到cv.notify_all();并且mutex已经被unlock,才会继续进行线程运行。
而std::unique_lock相比std::lock_guard提供了更多的灵活性,因为它允许延迟锁定、手动控制锁定和解锁、条件等待以及所有权转移。这使得std::unique_lock在需要更精细控制锁定时非常有用,比如在需要响应中断或异常处理时。
此外,std::unique_lock还可以与std::defer_lock、std::try_to_lock和std::adopt_lock标签配合使用,以在构造时指定不同的锁定行为。例如,使用std::defer_lock标签可以在构造时不锁定互斥锁,稍后再通过调用lock()方法来锁定。
C++ 多线程笔记2 线程同步的更多相关文章
- .NET面试题解析(07)-多线程编程与线程同步
系列文章目录地址: .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引 关于线程的知识点其实是很多的,比如多线程编程.线程上下文.异步编程.线程同步构造.GUI的跨线程访问等等, ...
- java核心知识点学习----多线程并发之线程同步
1.什么是线程同步? 多线程编程是很有趣的事情,它很容易出现"错误情况",这种情况不是由编码造成的,它是由系统的线程调度造成的,当使用多个线程来访问同一个数据时,很容易出现&quo ...
- .NET面试题解析(07)-多线程编程与线程同步 (转)
http://www.cnblogs.com/anding/p/5301754.html 系列文章目录地址: .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引 关于线程的知识点其实 ...
- java笔记--关于线程同步(7种同步方式)
关于线程同步(7种方式) --如果朋友您想转载本文章请注明转载地址"http://www.cnblogs.com/XHJT/p/3897440.html"谢谢-- 为何要使用同步? ...
- 细说C#多线程那些事 - 线程同步和多线程优先级
上个文章分享了一些多线程的一些基础的知识,今天我们继续学习. 一.Task类 上次我们说了线程池,线程池的QueueUserWorkItem()方法发起一次异步的线程执行很简单 但是该方法最大的问题是 ...
- java笔记--关于线程同步(5种同步方式)【转】
为何要使用同步? java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完 ...
- Win32多线程编程(3) — 线程同步与通信
一.线程间数据通信 系统从进程的地址空间中分配内存给线程栈使用.新线程与创建它的线程在相同的进程上下文中运行.因此,新线程可以访问进程内核对象的所有句柄.进程中的所有内存以及同一个进程中其他所有线 ...
- C# 多线程编程第二步——线程同步与线程安全
上一篇博客学习了如何简单的使用多线程.其实普通的多线程确实很简单,但是一个安全的高效的多线程却不那么简单.所以很多时候不正确的使用多线程反倒会影响程序的性能. 下面先看一个例子 : class Pro ...
- java多线程二之线程同步的三种方法
java多线程的难点是在:处理多个线程同步与并发运行时线程间的通信问题.java在处理线程同步时,常用方法有: 1.synchronized关键字. 2.Lock显示加锁. 3.信号量Se ...
- java笔记--关于线程同步(5种同步方式)
转自:http://www.2cto.com/kf/201408/324061.html 为何要使用同步? java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改 ...
随机推荐
- SqlSugar新增数据
1.插入方式 1.1 单条插入实体 //返回插入行数 db.Insertable(insertObj).ExecuteCommand(); //都是参数化实现 //异步: await db.Inser ...
- 强化学习从基础到进阶-案例与实践[1]:强化学习概述、序列决策、动作空间定义、策略价值函数、探索与利用、Gym强化学习实验
强化学习从基础到进阶-案例与实践[1]:强化学习概述.序列决策.动作空间定义.策略价值函数.探索与利用.Gym强化学习实验 1.1 强化学习概述 强化学习(reinforcement learning ...
- 7.2 Windows驱动开发:内核注册并监控对象回调
在笔者上一篇文章<内核枚举进程与线程ObCall回调>简单介绍了如何枚举系统中已经存在的进程与线程回调,本章LyShark将通过对象回调实现对进程线程的句柄监控,在内核中提供了ObRegi ...
- Python 实现进制转换与反汇编
通过利用反汇编库,并使用python编写工具,读取PE结构中的基地址偏移地址,找到OEP并计算成FOA文件偏移,使用反汇编库对其进行反汇编,并从反汇编代码里查找事先准备好的ROP绕过代码,让其自动完成 ...
- 分布式压测之locust和Jmeter的使用
受限于单台机器的配置问题,我们在单台机器上达不到一个很高的压测并发数,那这个时候就需要引入分布式压测 分布式压测原理: 一般通过局域网把不同测试计算机链接到一起,达到测试共享.分散操作.集中管理的目的 ...
- 699元 光威推出神武RGB系列DDR5 6400内存:海力士精选颗粒
光威推出了神武RGB系列DDR5 6400台式机内存条,售价为699元. 据了解,新款内存条采用了海力士M-die特挑颗粒,拥有CL-32-39-39-102低时序. 散热方面,这款内存条采用显卡级散 ...
- 【算法】【C语言进阶】C语言字符串操作宝藏级别汇总 strtok函数 strstr函数该怎么用?【超详细的使用解释和模拟实现】
[算法][C语言进阶]C语言字符串操作宝藏级别汇总[超详细的使用解释和模拟实现] 作者: @小小Programmer 这是我的主页:@小小Programmer 在食用这篇博客之前,博主在这里介绍一下其 ...
- ch583/ch582/ch573/ch571 central(主机)程序
本程序是在CH582m上运行的, 一.主从连接 主机这里可以根据从机的MAC地址进行连接.static uint8_t PeerAddrDef[B_ADDR_LEN] = {0x02, 0x02, 0 ...
- docker 安装 jenkins
最近在做一个智能床垫的项目,为了方便测试上周客户新给了一台华为云服务器.要求在一天内把之前阿里云服务器的环境以及java应用迁到新服务器.所以,我就需要在新服务器安装redis,mysql,jenki ...
- 《ASP.NET Core 与 RESTful API 开发实战》-- (第7章)-- 读书笔记(下)
第 7 章 高级主题 7.4 HATEOAS 全称 Hypermedia AS The Engine Of Application State,即超媒体作为应用程序状态引擎.它作为 REST 统一界面 ...