【Linux初阶】多线程4 | POSIX信号量,基于环形队列的生产消费模型,线程池,线程安全的单例模式,STL-智能指针和线程安全

news2025/1/15 6:57:00

在这里插入图片描述

文章目录

  • ☀️一、POSIX信号量
    • 🌻1.引入
    • 🌻2.信号量的概念
    • 🌻3.信号量函数
  • ☀️二、基于环形队列的生产消费模型
    • 🌻1.理解环形队列
    • 🌻2.代码案例
  • ☀️三、线程池
  • ☀️四、线程安全的单例模式
    • 🌻1.单例模式与设计模式
    • 🌻2.饿汉实现方式和懒汉实现方式
  • ☀️五、STL,智能指针和线程安全
  • ☀️六、其他常见的各种锁(了解)


☀️一、POSIX信号量

🌻1.引入

  • 回顾我们之前学习的线程知识,我们知道一个线程访问临界资源时,是需要满足生产消费条件的。
  • 因此我们对公共资源的访问逻辑一般为:加锁、检测、操作(等待或执行)、解锁
  • 那么我们有没有一种办法,可以让我们提前得知检测条件是否满足呢?这就要涉及到我们今天要学习的知识 - 信号量了。

🌻2.信号量的概念

  • 什么是信号量

    • 信号量本质上就是一把计数器

    • 是一把衡量衡量临界资源中资源多少的计数器。

    • 我们对临界资源进行整体加锁,就默认了,我们对这个资源是整体使用的。但是实际情况可能是,只有一份公共资源,但是允许访问资源不同区域的情况。

    • 申请信号量的本质:对临界资源中特定小块资源进行 预定 的机制(类似于网上预定电影票)。

    • 通过先申请信号量(计数器),再执行的方式,就可以实现在未来某个线程占有一部分临界资源,与其他线程占用同一临界资源的其他部分不冲突。

  • 信号量底层原理

    • 设 sem_t sem = 10,sem_t是计数器的类型,sem = 10表示这块临界资源可以分为10块小资源。

    • sem–,申请资源,必须保证操作的原子性,我们用 P代表这个操作。

    • sem++,归还资源,必须保证操作的原子性,我们用 V代表这个操作。

    • 信号量核心操作:PV原语。

🌻3.信号量函数

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步

  • 初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
	pshared:0表示线程间共享,非零表示进程间共享
	value:信号量初始值
  • 销毁信号量
int sem_destroy(sem_t *sem);
  • 等待信号量
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
  • 发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1int sem_post(sem_t *sem);//V()

☀️二、基于环形队列的生产消费模型

🌻1.理解环形队列

  • 环形队列采用数组模拟,用模运算来模拟环状特性

在这里插入图片描述

数组有num格空间,当 i指向 num格的位置的时候,%(模)num,就可以回到起始位置

  • 生产和消费什么时候能访问同一个位置

    1. 队列空的时候。
    2. 队列满的时候。
    3. 其他情况,生产者和消费者,根本上访问的区域是不同的。
  • 环形队列的生产消费问题需要 遵守的规则/完成的核心工作

    1. 生产者不能超过消费者。
    2. 消费者不能将生产者套一个圈及以上。
    3. 生产者消费者指向统一资源的情况:a.队列全空(生产者需要先执行);b.队列全满(消费者需要先执行)。

在环形队列当中,大部分情况下,即满足上述规则,单生产和单消费是可以并发执行的

  • 信号量是用来衡量临界资源中的资源数量的
    1. 对于生产者而言看重什么?队列中的剩余空间 – 空间资源定义一个信号量。
    2. 对于消费者而言看重什么?放入队列中的数据 – 数据资源定义一个信号量。

在这里插入图片描述

我们给生产者和消费者各设定一个计数器,就可以很简单的进行多线程间的同步过程。

  • 双信号量运行原理(伪代码)

    对生产者而言:
        prodocter_sem = 10	//申请信号量,成功则向下运行,失败则阻塞
        comsumer_sem = 0;
        
        P(prodocter_sem);	//prodocter_sem--
    
    	//从事生产活动 -- 把数据放到队列中
    
    	V(comsumer_sem)//comsumer_sem++
            
    对消费者而言:
        P(comsumer_sem);	//comsumer_sem--
    
    	//从事消费活动 -- 把数据从队列中拿出
    
    	V(prodocter_sem)//prodocter_sem++
    

​ 当我们的生产信号量减到0时,再申请空间资源就申请不到了,因此就可以满足生产消费的三个条件。

🌻2.代码案例

  • 我们现在有信号量这个计数器,就可以很简单的进行多线程间的同步过程。
  • 下面的代码例子是两个线程分别同时进行生产消费。
#include <iostream>
#include <vector>
#include <stdlib.h>
#include <semaphore.h>
#include <pthread.h>

#define NUM 16

class RingQueue{
private:
	std::vector<int> q;
	int cap;
	sem_t data_sem;
	sem_t space_sem;
	int consume_step;
	int product_step;

public:
	RingQueue(int _cap = NUM) :q(_cap), cap(_cap)
	{
		sem_init(&data_sem, 0, 0);
		sem_init(&space_sem, 0, cap);
		consume_step = 0;
		product_step = 0;
	}
	void PutData(const int& data)
	{
		sem_wait(&space_sem); // P
		q[consume_step] = data;
		consume_step++;
		consume_step %= cap;
		sem_post(&data_sem); //V
	}
	void GetData(int& data)
	{
		sem_wait(&data_sem);
		data = q[product_step];
		product_step++;
		product_step %= cap;
		sem_post(&space_sem);
	}
	~RingQueue()
	{
		sem_destroy(&data_sem);
		sem_destroy(&space_sem);
	}
};

void* consumer(void* arg)
{
	RingQueue* rqp = (RingQueue*)arg;
	int data;
	for (; ; ) {
		rqp->GetData(data);
		std::cout << "Consume data done : " << data << std::endl;
		sleep(1);
	}
}

//more faster
void* producter(void* arg)
{
	RingQueue* rqp = (RingQueue*)arg;
	srand((unsigned long)time(NULL));
	for (; ; ) {
		int data = rand() % 1024;
		rqp->PutData(data);
		std::cout << "Prodoct data done: " << data << std::endl;
		// sleep(1);
	}
}

int main()
{
	RingQueue rq;
	pthread_t c, p;

	pthread_create(&c, NULL, consumer, (void*)&rq);
	pthread_create(&p, NULL, producter, (void*)&rq);

	pthread_join(c, NULL);
	pthread_join(p, NULL);
}

☀️三、线程池

/*threadpool.h*/
/* 线程池:
*一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着
监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利
用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

* 线程池的应用场景:
*	1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技
术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个
Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
*	2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
*	3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情
况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,
出现错误.

* 线程池的种类:
* 线程池示例:
*	1. 创建固定数量线程池,循环从任务队列中获取任务对象,
*	2. 获取到任务对象后,执行任务对象中的任务接口
*/
/*threadpool.hpp*/
#ifndef __M_TP_H__
#define __M_TP_H__
#include <iostream>
#include <queue>
#include <pthread.h>

#define MAX_THREAD 5
typedef bool (*handler_t)(int);

class ThreadTask
{
private:
	int _data;
	handler_t _handler;
public:
	ThreadTask() :_data(-1), _handler(NULL) {}
	ThreadTask(int data, handler_t handler) {
		_data = data;
		_handler = handler;
	}
	void SetTask(int data, handler_t handler) {
		_data = data;
		_handler = handler;
	}
	void Run() {
		_handler(_data);
	}
};

class ThreadPool
{
private:
	int _thread_max;
	int _thread_cur;
	bool _tp_quit;
	std::queue<ThreadTask*> _task_queue;
	pthread_mutex_t _lock;
	pthread_cond_t _cond;
private:
	void LockQueue() {
		pthread_mutex_lock(&_lock);
	}
	void UnLockQueue() {
		pthread_mutex_unlock(&_lock);
	}
	void WakeUpOne() {
		pthread_cond_signal(&_cond);
	}
	void WakeUpAll() {
		pthread_cond_broadcast(&_cond);
	}
	void ThreadQuit() {
		_thread_cur--;
		UnLockQueue();
		pthread_exit(NULL);
	}
	void ThreadWait() {
		if (_tp_quit) {
			ThreadQuit();
		}
		pthread_cond_wait(&_cond, &_lock);
	}
	bool IsEmpty() {
		return _task_queue.empty();
	}
	static void* thr_start(void* arg) {
		ThreadPool* tp = (ThreadPool*)arg;
		while (1) {
			tp->LockQueue();
			while (tp->IsEmpty()) {
				tp->ThreadWait();
			}
			ThreadTask* tt;
			tp->PopTask(&tt);
			tp->UnLockQueue();
			tt->Run();
			delete tt;
		}
		return NULL;
	}

public:
	ThreadPool(int max = MAX_THREAD) :_thread_max(max), _thread_cur(max),
		_tp_quit(false) {
		pthread_mutex_init(&_lock, NULL);
		pthread_cond_init(&_cond, NULL);
	}
	~ThreadPool() {
		pthread_mutex_destroy(&_lock);
		pthread_cond_destroy(&_cond);
	}
	bool PoolInit() {
		pthread_t tid;
		for (int i = 0; i < _thread_max; i++) {
			int ret = pthread_create(&tid, NULL, thr_start, this);
			if (ret != 0) {
				std::cout << "create pool thread error\n";
				return false;
			}
		}
		return true;
	}
	bool PushTask(ThreadTask* tt) {
		LockQueue();
		if (_tp_quit) {
			UnLockQueue();
			return false;
		}
		_task_queue.push(tt);
		WakeUpOne();
		UnLockQueue();
		return true;
	}
	bool PopTask(ThreadTask** tt) {
		*tt = _task_queue.front();
		_task_queue.pop();
		return true;
	}
	bool PoolQuit() {
		LockQueue();
		_tp_quit = true;
		UnLockQueue();
		while (_thread_cur > 0) {
			WakeUpAll();
			usleep(1000);
		}
		return true;
	}
};
#endif
/*main.cpp*/
bool handler(int data)
{
	srand(time(NULL));
	int n = rand() % 5;
	printf("Thread: %p Run Tast: %d--sleep %d sec\n", pthread_self(), data, n);
	sleep(n);
	return true;
}
int main()
{
	int i;
	ThreadPool pool;
	pool.PoolInit();
	for (i = 0; i < 10; i++) {
		ThreadTask* tt = new ThreadTask(i, handler);
		pool.PushTask(tt);
	}
	pool.PoolQuit();
	return 0;
}
g++ -std=c++0x test.cpp -o test -pthread -lrt

☀️四、线程安全的单例模式

🌻1.单例模式与设计模式

  • 什么是单例模式

单例模式是一种 “经典的, 常用的, 常考的” 设计模式

  • 什么是设计模式

IT行业这么火,涌入的人很多,俗话说林子大了啥鸟都有。大佬和菜鸡们两极分化的越来越严重,为了让菜鸡们不太拖大佬的后腿,于是大佬们针对一些经典的常见的场景,给定了一些对应的解决方案,这个就是 设计模式

  • 单例模式的特点

某些类, 只应该具有一个对象(实例), 就称之为单例.

例如一个男人只能有一个媳妇.

在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.

🌻2.饿汉实现方式和懒汉实现方式

[洗碗的例子]

吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.

懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度.

  • 饿汉方式实现单例模式
template <typename T>
class Singleton {
	static T data;
public:
	static T* GetInstance() 
	{
		return &data;
	}
};

只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例.

  • 懒汉方式实现单例模式
template <typename T>
class Singleton {
	static T* inst;
public:
	static T* GetInstance() 
    {
		if (inst == NULL) 
    	{
			inst = new T();
		}
        return inst;
	}
};

上述代码存在一个严重的问题, 线程不安全.

第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例.

但是后续再次调用, 就没有问题了.

  • 懒汉方式实现单例模式(线程安全版本)
// 懒汉模式, 线程安全
template <typename T>
class Singleton {
	volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
	static std::mutex lock;
public:
	static T* GetInstance() {
		if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提高性能.
			lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
			if (inst == NULL) {
				inst = new T();
			}
			lock.unlock();
		}
		return inst;
	}
};

注意事项:

  1. 加锁解锁的位置
  2. 双重 if 判定, 避免不必要的锁竞争
  3. volatile关键字防止过度优化

☀️五、STL,智能指针和线程安全

  • STL中的容器是否是线程安全的?

不是.

原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.

而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).

因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.

  • 智能指针是否是线程安全的?

对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.

对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.


☀️六、其他常见的各种锁(了解)

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 自旋锁,公平锁,非公平锁…

🌹🌹 多线4 的知识大概就讲到这里啦,博主后续会继续更新更多C++ 和 Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1104861.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

内网渗透-哈希传递

文章目录 哈希传递概念LMNTLM 原理利用hash传递 浏览上传文件实操&#xff1a;使用域中的一台机器通过哈希传递查看域控主机的C盘目录 hash传递获取域控RDPhash传递获取域控RDP 哈希传递 概念 早期SMB协议铭文在网络上传输数据&#xff0c;后来诞生了LM验证机制&#xff0c;L…

FPGA ZYNQ VIVADO创建IP核点亮LED灯 方式一

这里写自定义目录标题 PL端 纯Verilog语言创建IP核实现点亮LED灯工使用设备 ZYNQ 7010&#xff0c;选择设备型号XC7Z010CLG400-1根据以下流程完成本次创建时钟频率50MHZ&#xff0c;周期T20ns&#xff0c;因此计数50_000_000次&#xff0c;1sLED灯闪烁一次 PL端 纯Verilog语言创…

解决 Windows 7 激活信息失败报错 0xC004F057

文章目录 步骤一&#xff1a;以管理员身份运行命令提示符步骤二&#xff1a;卸载当前密钥信息步骤三&#xff1a;清除产品密钥信息步骤四&#xff1a;重新启动 Windows Activation Technologies 服务步骤五&#xff1a;重启电脑 &#x1f389;解决 Windows 7 激活信息失败报错 …

一款超好用的AI Logo生成器,免费可商用

HI&#xff0c;同学们&#xff0c;我是赤辰&#xff0c;本期是第20篇AI工具类教程&#xff0c;文章底部准备了粉丝福利&#xff0c;看完后可领取&#xff01; 在职场中&#xff0c;常常免不了需要进行Logo设计和制作&#xff0c;特别是对于非专业人员来说&#xff0c;设计Logo…

C语言文件操作(上)

文章目录 一、为什么使用文件二、什么是文件1.程序文件2.数据文件3.文件名 三、文件的打开与关闭1.文件指针2.文件的打开和关闭fopen 与 fclose 四、文件的顺序读写01 字符输出函数&#xff1a;fputs02 字符输入函数&#xff1a;fgetc03 文本行输出函数&#xff1a;fputs04 文本…

【计算机网络笔记】计算机网络性能(2)——时延带宽积、丢包率、吞吐量/率

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 系列文章目录时延带宽积丢包率吞吐量/率&am…

探讨Acrel-1000DP分布式光伏系统的设计与应用-安科瑞 蒋静

摘 要&#xff1a;分布式光伏发电特指在用户场地附近建设&#xff0c;运行方式以用户侧自发自用、余电上网&#xff0c;且在配电系统平衡调节为特征的光伏发电设施&#xff0c;是一种新型的、具有广阔发展前景的发电和能源综合利用方式&#xff0c;它倡导就近发电&#xff0c;就…

数据可视化工具 ,不会写 SQL 代码也能做数据分析

数据可视化工具可以帮助人们以直观、易于理解的方式展现和分析数据。这些工具使得即使不会写 SQL 代码的人也能进行数据分析&#xff0c;并从中获得有价值的信息和见解。 本文将详细介绍几种常用的数据可视化工具及其功能和优点。 1. Datainside: Datainside是一款流行的数…

jupyter崩溃OOM,out of memory,jupyter代码写不进去,保存不了。

最近写一个比较长的数据处理代码&#xff0c;有快千行&#xff0c;然后经常代码没有写入&#xff0c;然后直接网页崩溃&#xff0c;给我干蒙了。我已经是jupyter版本的问题&#xff0c;弄了半天&#xff0c;弄完&#xff0c;还是有这个问题。然后就查了一下&#xff0c;发现是j…

【操作系统】虚拟内存串讲

文章目录 概述虚拟页管理请求页表物理地址的获取虚拟页大小与内存块大小的探讨 概述 操作系统为每一个进程分配一个独立的虚拟内存空间&#xff0c;以分页系统为例&#xff0c;每个进程的虚拟页号都是从 0 开始的 不同的进程可以使用相同的虚拟页号&#xff0c;并且不会互相影…

Redis的BitMap实现分布式布隆过滤器

布隆过滤器&#xff08;Bloom Filter&#xff09;是一种高效的概率型数据结构&#xff0c;用于判断一个元素是否属于一个集合。它通过使用哈希函数和位数组来存储和查询数据&#xff0c;具有较快的插入和查询速度&#xff0c;并且占用空间相对较少。 引入依赖 <!--切面--&…

Spring Cloud 之 Feign 简介及简单DEMO的搭建

Feign简介&#xff1a; Feign是一种声明式、模板化的HTTP客户端。在Spring Cloud中使用Feign, 我们可以做到使用HTTP请求远程服务时能与调用本地方法一样的编码体验。 Feign是在RestTemplate基础上封装的&#xff0c;使用注解的方式来声明一组与服务提供者Rest接口所对应的本地…

黑豹程序员-架构师学习路线图-百科:Maven

文章目录 1、什么是maven官网下载地址 2、发展历史3、Maven的伟大发明 1、什么是maven Apache Maven is a software project management and comprehension tool. Based on the concept of a project object model (POM), Maven can manage a project’s build, reporting and…

微信小程序3

一、flex布局 布局的传统解决方案&#xff0c;基于[盒状模型]&#xff0c;依赖display属性 position属性 float属性 1、什么是flex布局&#xff1f; Flex是Flexible Box的缩写&#xff0c;意为”弹性布局”&#xff0c;用来为盒状模型提供最大的灵活性。 任何一个容器都可以…

同为科技(TOWE)工业用插头插座与连接器产品大全

TOWE IPS系列工业标准插头插座、连接器系列产品 随着国内经济快速的发展&#xff0c;人们生活水平的不断提高&#xff0c;基础设施的建设是发展的基础&#xff0c;完善的基础设施对加速经济的发展起到至关重要的作用。其中&#xff0c;基础建设中机场、港口、电力、通讯等公共…

GLEIF携手TrustAsia,共促数字邮件证书的信任与透明度升级

TrustAsia首次发布嵌入LEI的S/MIME证书&#xff0c;用于验证法定实体相关的电子邮件账户的真实与完整性 2023年10月&#xff0c;全球法人识别编码基金会&#xff08;GLEIF&#xff09;与证书颁发机构&#xff08;CA&#xff09;TrustAsia通力合作&#xff0c;双方就促进LEI在数…

基础课5——语音合成技术

TTS是语音合成技术的简称&#xff0c;也称为文语转换或语音到文本。它是指将文本转换为语音信号&#xff0c;并通过语音合成器生成可听的语音。TTS技术可以用于多种应用&#xff0c;例如智能语音助手、语音邮件、语音新闻、有声读物等。 TTS技术通常包括以下步骤&#xff1a; …

医学大数据分析 - 心血管疾病分析 计算机竞赛

文章目录 1 前言1 课题背景2 数据处理3 数据可视化4 最后 1 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于大数据的心血管疾病分析 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f9…

什么牌子的电容笔性价比高?电容笔牌子排行

在科技进步的同时&#xff0c;各种类型的电容笔也在国内的市场上涌现。一支好用的电容笔&#xff0c;不仅能让我们在学习上有很大的提高&#xff0c;而且还能让我们的工作效率大大提高。国产平替电容笔&#xff0c;在技术和品质上&#xff0c;都有很大的改进余地&#xff0c;起…

如何才能拥有大量的虾皮印尼买家号?

注册虾皮印尼买家号还是比较简单的&#xff0c;直接打开shopee印尼官网&#xff0c;点击注册&#xff0c;输入手机号&#xff0c;接收短信&#xff0c;然后再设置一个密码就可以了。 如果想要注册多个虾皮买家号&#xff0c;那么要借助软件操作才可以&#xff0c;比如shopee买家…