Linux 12:多线程2

1. 生产者消费者模型

        生产者消费者模型有三种关系,两个角色,一个交易场所。

三种关系:

        生产者之间是什么关系?竞争 - 互斥

        消费者和消费者之间?竞争 - 互斥

        消费者和消费者之间?互斥和同步

两个角色:

        生产者和消费者

一个交易场所:

        内存空间

1-1. 为何要使用生产者消费者模型

        生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

1-2. 生产者消费者模型优点

  •  解耦
  • 支持并发
  • 支持忙闲不均

2. 基于BlockingQueue的生产者消费者模型

BlockingQueue

        在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)。

2-1. C++ queue模拟阻塞队列的生产消费模型

        为了便于理解,我们以单生产者,单消费者,来进行讲解,代码:

#include <iostream>
#include <queue>
#include <stdlib.h>
#include <pthread.h>
#define NUM 8
class BlockQueue
{
private:std::queue<int> q;int cap;pthread_mutex_t lock;pthread_cond_t full;pthread_cond_t empty;private:void LockQueue(){pthread_mutex_lock(&lock);}void UnLockQueue(){pthread_mutex_unlock(&lock);}void ProductWait(){pthread_cond_wait(&full, &lock);}void ConsumeWait(){pthread_cond_wait(&empty, &lock);}void NotifyProduct(){pthread_cond_signal(&full);}void NotifyConsume(){pthread_cond_signal(&empty);}bool IsEmpty(){return (q.size() == 0 ? true : false);}bool IsFull(){return (q.size() == cap ? true : false);}public:BlockQueue(int _cap = NUM) : cap(_cap){pthread_mutex_init(&lock, NULL);pthread_cond_init(&full, NULL);pthread_cond_init(&empty, NULL);}voidPushData(const int &data){LockQueue();while (IsFull()){NotifyConsume();std::cout << "queue full, notify consume data, product stop." << std::endl;ProductWait();}q.push(data);// NotifyConsume();UnLockQueue();}voidPopData(int &data){LockQueue();while (IsEmpty()){NotifyProduct();std::cout << "queue empty, notify product data, consume stop." << std::endl;ConsumeWait();}data = q.front();q.pop();// NotifyProduct();UnLockQueue();}~BlockQueue(){pthread_mutex_destroy(&lock);pthread_cond_destroy(&full);pthread_cond_destroy(&empty);}
};
void *consumer(void *arg)
{BlockQueue *bqp = (BlockQueue *)arg;int data;for (;;){bqp->PopData(data);std::cout << "Consume data done : " << data << std::endl;}
}
// more faster void *producter(void *arg)
{BlockQueue *bqp = (BlockQueue *)arg;srand((unsigned long)time(NULL));for (;;){int data = rand() % 1024;bqp->PushData(data);std::cout << "Prodoct data done: " << data << std::endl;// sleep(1);}
}
int main()
{BlockQueue bq;pthread_t c, p;pthread_create(&c, NULL, consumer, (void *)&bq);pthread_create(&p, NULL, producter, (void *)&bq);pthread_join(c, NULL);pthread_join(p, NULL);return 0;
}

3. POSIX信号量

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

3-1. 初始化信号量

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

参数:

        pshared:0表示线程间共享,非零表示进程间共享

        value:信号量初始值

3-2. 销毁信号量

 int sem_destroy(sem_t *sem);

3-3. 等待信号量

功能:等待信号量,会将信号量的值减1

int sem_wait(sem_t *sem); //P()

3-4. 发布信号量

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。

int sem_post(sem_t *sem);//V()

        上一节生产者-消费者的例子是基于queue的,其空间可以动态分配,现在基于固定大小的环形队列重写这个程序(POSIX信号量):
 

4. 基于环形队列的生产消费模型

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

        环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态。

         但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程。

#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); // Pq[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);
}

5. 线程池

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

线程池的应用场景:

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

 线程池示例:

  1. 创建固定数量线程池,循环从任务队列中获取任务对象,
  2. 获取到任务对象后,执行任务对象中的任务接口

threadpool.hpp

#pragma once
#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();}T hreadTask *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;}
};

main.cpp

#include "threadpool.hpp"
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
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;
}

6. 线程安全的单例模式

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

6-1. 单例模式的特点

        某些类, 只应该具有一个对象(实例),就称之为单例。
        在很多服务器开发场景中,经常需要让服务器加载很多的数据 (上百G) 到内存中。此时往往要用一个单例的类来管理这些数据。

6-2. 饿汉实现方式和懒汉实现方式 

        吃完饭,立刻洗碗,这种就是饿汉方式。
        吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗,就是懒汉方式。

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

 6-2-1. 饿汉方式实现单例模式

template <typename T>

class Singleton {

        static T data;

public:

        static T* GetInstance() {

                return &data;

        }

};

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

6-2-2. 懒汉方式实现单例模式

template <typename T>

class Singleton {

        static T* inst;

public:

        static T* GetInstance() {

        if (inst == NULL) {

                inst = new T();

        }

        return inst;

        }

};

  • 存在一个严重的问题,线程不安全。
  • 第一次调用GetInstance的时候,如果两个线程同时调用,可能会创建出两份T对象的实例。
  • 但是后续再次调用,就没有问题了。

 6-2-3. 懒汉方式实现单例模式(线程安全版本)

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关键字防止过度优化。

7. STL,智能指针和线程安全

7-1.STL中的容器是否是线程安全的?

        不是。
        原因是,STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。
        而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。
        因此STL默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。

7-2. 智能指针是否是线程安全的?

        对于unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题。
        对于shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证shared_ptr能够高效,原子的操作引用计数。

 8. 其他常见的各种锁

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Apache POI】Java解析Excel文件并处理合并单元格-粘贴即用

同为牛马&#xff0c;点个赞吧&#xff01; 一、Excel文件样例 二、工具类源码 import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.xssf.usermodel.XSSFWorkbookFactory; import org.springframework.web.multip…

mac M1 创建Mysql8.0容器

MySLQ8.0 拉取m1镜像 docker pull mysql:8.0创建挂载文件夹并且赋予权限 sudo chmod 777 /Users/zhao/software/dockerLocalData/mysql 创建容器并且挂载 docker run --name mysql_8 \-e MYSQL_ROOT_PASSWORDadmin \-v /Users/zhao/software/dockerLocalData/mysql/:/var/l…

利用patch-package补丁,解决H5预览PDF时电子签章不显示问题

利用patch-package补丁&#xff0c;解决H5预览PDF时电子签章不显示问题 一、问题描述 在生产环境中&#xff0c;遇到了一个紧急的技术问题&#xff1a;用户在移动端H5页面上查看电子票时&#xff0c;PDF文件预览功能正常&#xff0c;但其中的电子签章未能正常显示。这一问题直…

C++ AVL树

目录 ​编辑 0.前言 1.AVL树的概念 1.1 平衡因子 1.2 AVL树的性质 2.AVL树节点的定义 3.AVL树的插入 4.AVL树的旋转 4.1 左单旋&#xff08;LL旋转&#xff09; 4.2 右单旋&#xff08;RR旋转&#xff09; 4.3 右左旋&#xff08;RL旋转&#xff09; 4.4 左右旋&…

集群架构-web服务器(接入负载均衡+数据库+会话保持redis)--15454核心配置详解

紧接着前面的集群架构深化—中小型公司&#xff08;拓展到大型公司业务&#xff09;–下面图简单回顾一下之前做的及故障核心知识总结&#xff08;等后期完全整理后&#xff0c;上传资源希望能帮大家&#xff09; web集群架构-接入负载均衡部署web02服务器等 web集群-搭建web0…

介绍 Elasticsearch 中的 Learning to Tank - 学习排名

作者&#xff1a;来自 Elastic Aurlien Foucret 从 Elasticsearch 8.13 开始&#xff0c;我们提供了原生集成到 Elasticsearch 中的学习排名 (learning to rank - LTR) 实现。LTR 使用经过训练的机器学习 (ML) 模型为你的搜索引擎构建排名功能。通常&#xff0c;该模型用作第二…

postman接口测试实战篇

击杀小游戏接口测试 接口测试简单介绍击杀小游戏代码下载单接口测试(postman)接口关联并参数化接口测试简单介绍 首先思考两个问题:1.接口是什么?2.接口测试是什么? 1.我们总是把接口想的很复杂,其实呢,它就是一个有特定输入和输出参数的交互逻辑处理单元,它不需要知…

通过 EMR Serverless Spark 提交 PySpark 流任务

在大数据快速发展的时代&#xff0c;流式处理技术对于实时数据分析至关重要。EMR Serverless Spark提供了一个强大而可扩展的平台&#xff0c;它不仅简化了实时数据处理流程&#xff0c;还免去了服务器管理的烦恼&#xff0c;提升了效率。本文将指导您使用EMR Serverless Spark…

PostgreSQL使用(二)

说明&#xff1a;本文介绍PostgreSQL的DML语言&#xff1b; 插入数据 -- 1.全字段插入&#xff0c;字段名可以省略 insert into tb_student values (1, 张三, 1990-01-01, 88.88);-- 2.部分字段插入&#xff0c;字段名必须写全 insert into tb_student (id, name) values (2,…

[数据集][目标检测]导盲犬拐杖检测数据集VOC+YOLO格式4635张2类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;4635 标注数量(xml文件个数)&#xff1a;4635 标注数量(txt文件个数)&#xff1a;4635 标注…

graham 算法计算平面投影点集的凸包

文章目录 向量的内积&#xff08;点乘&#xff09;、外积&#xff08;叉乘&#xff09;确定旋转方向numpy 的 cross 和 outernp.inner 向量与矩阵计算示例np.outer 向量与矩阵计算示例 python 示例生成样例散点数据图显示按极角排序的结果根据排序点计算向量转向并连成凸包 基本…

Linux云计算 |【第一阶段】ENGINEER-DAY3

主要内容&#xff1a; LVM逻辑卷管理、VDO、RAID磁盘阵列、进程管理 一、新建逻辑卷 1、什么是逻辑卷 逻辑卷&#xff08;Logical Volume&#xff09;是逻辑卷管理&#xff08;Logical Volume Management&#xff0c;LVM&#xff09;系统中的一个概念。LVM是一种用于磁盘管理…

C++ :友元类

友元类的概念和使用 (1)将类A声明为B中的friend class后&#xff0c;则A中所有成员函数都成为类B的友元函数了 (2)代码实战&#xff1a;友元类的定义和使用友元类是单向的 (3)友元类是单向的&#xff0c;代码实战验证 互为友元类 (1)2个类可以互为友元类&#xff0c;代码实战…

Intel和AMD用户再等等!微软确认Win11 24H2年底前登陆

微软近日确认&#xff0c;Windows 11 24H2版本将于2024年底前正式登陆使用英特尔和AMD处理器的PC。 根据微软介绍&#xff0c;Windows 11 24H2将作为传统功能更新&#xff0c;将在今年晚些时候提供给所有设备。 此前&#xff0c;微软已向搭载骁龙X Plus和X Elite系列处理器的Co…

VS2019安装MFC组件

VS2019支持的MFC版本是mfc140 ~ mfc142版本&#xff0c;它兼容VS2015、VS2017之前的老版本程序。 一、MFC的历史版本 MFC的历史版本如下&#xff1a; IDE发布时间工具集版本MSC_VERMSVCMFC版本dllVisual C6.01998V601200MSVC6.06.0mfc42.dll、mfcce400.dllVisual Studio 2002…

Linux的热插拔UDEV机制和守护进程

目录 一、Linux的热插拔UDEV机制 二、守护进程 2.1 守护进程概念和基本特点&#xff1a; 2.2 显示进程信息&#xff1a; 2.3 守护进程和后台进程的区别&#xff1a; 2.4 创建守护进程的步骤和守护进程的特征&#xff1a; 2.4.1 创建守护进程的步骤&#xff1a; 2.4.2 守…

前端不懂 Docker ?先用它换掉常规的 Vue 项目部署方式

本项目代码已开源&#xff0c;具体见&#xff1a; 前端工程&#xff1a;vue3-ts-blog-frontend 后端工程&#xff1a;express-blog-backend 数据库初始化脚本&#xff1a;关注公众号程序员白彬&#xff0c;回复关键字“博客数据库脚本”&#xff0c;即可获取。 为什么需要容器化…

如何在 Mac 上下载安装植物大战僵尸杂交版? 最新版本 2.2 详细安装运行教程问题详解

植物大战僵尸杂交版已经更新至2.2了&#xff0c;但作者只支持 Windows、手机等版本并没有支持 MAC 版本&#xff0c;最近搞到了一个最新的杂交 2.2 版本的可以在 Macbook 上安装运行的移植安装包&#xff0c;试了一下非常完美能够正常在 MAC 上安装运行&#xff0c;看图&#x…

Linux云计算 |【第一阶段】ENGINEER-DAY5

主要内容&#xff1a; SELinux、系统故障修复、HTTPD/FTP服务搭建、防火墙策略管理、服务管理 一、SELinux安全制度 SELinux&#xff08;Security-Enhanced Linux&#xff09;&#xff0c;美国NSA国家安全局主导开发&#xff0c;一套增强Linux系统安全的强制访问控制体系&…

大模型只是轮子,与其闭门重复造轮子,不如深耕场景应用

如何理解李彦宏说的“不要卷模型&#xff0c;要卷应用” 7月4日&#xff0c;2024世界人工智能大会暨人工智能全球治理高级别会议全体会议在上海世博中心举办。在产业发展主论坛上&#xff0c;百度创始人、董事长兼首席执行官李彦宏呼吁&#xff1a;“大家不要卷模型&#xff0…