【Linux】线程池线程安全的单例模式和STL读者写者问题

需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)

文章目录

  • 1. 线程池
    • 1.1 线程池是什么
    • 1.2 为什么要有线程池
    • 1.3 线程池的应用场景
    • 1.4 线程池的任务
    • 1.5 线程池的代码实现
  • 2. 线程安全的单例模式
  • 3. STL、智能指针和线程安全
  • 4. 其他常见锁的了解
  • 5. 读者写者问题

1. 线程池

1.1 线程池是什么

线程池是一种线程的使用方式,是一种池化技术,在很早之前,我们其实接触过一些池化技术的例子,比如STL容器内部实现的alloctor就是一种内存的池化技术,将要用的空间首先申请一大批,然后再按照容器的需要从这个内存池中申请。还有在学习进程的时候,我们也尝试写过一些进程池的代码,一次创建一大批进程,按照要求分别执行不同的任务。当然这里也可以创建线程池,用线程来执行任务

1.2 为什么要有线程池

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

1.3 线程池的应用场景

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

1.4 线程池的任务

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

1.5 线程池的代码实现

/* ThreadPool.hpp 线程池的代码实现 */
#pragma once
#include "LockGuard.hpp"
#include "Thread.hpp"
#include <vector>
#include <queue>
#include <string>
#include <iostream>const int gnum = 5; // 线程池中默认的线程个数template <class T>
class ThreadPool; // 线程池类的声明/* 线程数据类,保存线程对应的内容包括线程池对象的指针和线程名 */
template <class T>
class ThreadData
{
public:ThreadData(ThreadPool<T> *tp, const std::string &n) : threadpool(tp), name(n){};public:ThreadPool<T> *threadpool;std::string name;
};/* 线程池类的实现 */
template <class T>
class ThreadPool
{
public:static void *handleTask(void *args) // 线程需要执行的回调函数{ThreadData<T> *td = static_cast<ThreadData<T> *>(args);while (true){T t; // 构建任务对象{LockGuard lockGuard(td->threadpool->mutex()); // 上锁while (td->threadpool->isQueueEmpty()){// 如果任务队列为空,线程挂起,等待队列中被填充任务td->threadpool->threadWait();}t = td->threadpool->pop(); // 如果队列中有任务,就拿出任务}// 任务在锁外执行std::cout << "获取了一个任务" << t.toTaskString() << "并执行了任务,结果是 " << t() << std::endl;}delete td;return nullptr;}public: // 给handleTask调用的外部接口pthread_mutex_t *mutex() { return &_mutex; }bool isQueueEmpty() { return _task_queue.empty(); }void threadWait() { pthread_cond_wait(&_cond, &_mutex); }T pop() // 获取线程池中任务队列里需要执行的下一个任务{T t = _task_queue.front();_task_queue.pop();return t;}public:                               // 需要暴露给外部的接口ThreadPool(const int &num = gnum) // 构造函数,初始化互斥量和条件变量,构建指定个数的Thread对象{pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);for (int i = 0; i < num; ++i){_threads.push_back(new Thread());}}void run() // 为所有线程对象创建真正的执行流,并执行对应的回调函数{for (const auto &thread : _threads){ThreadData<T> *td = new ThreadData<T>(this, thread->GetTaskName()); // 构造handleTask的参数对象thread->start(handleTask, td);                                      // 调用该线程的start函数,创建新线程执行指定的handleTask任务std::cout << thread->GetTaskName() << " start..." << std::endl;}}void push(const T &in) // 将指定任务push到队列中{// 加锁LockGuard lockGuard(&_mutex); // 自动加锁,在当前代码段结束之后调用LockGuard的析构函数解锁_task_queue.push(in);pthread_cond_signal(&_cond); // 发送信号表示此时task_queue中有值,让消费者可以使用}~ThreadPool() // 析构函数,销毁互斥量和条件变量,delete所有thread对象指针,自动调用thread对象的析构函数{pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);for (auto &thread : _threads){delete thread;}}private:std::vector<Thread *> _threads; // 保存所有线程对象的指针std::queue<T> _task_queue;      // 需要被分配的任务队列pthread_mutex_t _mutex;         // 任务队列需要被互斥的访问pthread_cond_t _cond;           // 生产任务和消费任务之间需要进行同步
};

构建和测试线程池用到的小组件

/* Task.hpp 线程对象,C++风格的线程库*/
#pragma once#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <cassert>class Thread
{
public:using func_t = std::function<void *(void *)>; // 定义func_t类型static int number;                            // 线程编号,按照一次运行时的调用次数计数
public:Thread(){char *buffer = new char[64];name_ = "thread-" + std::to_string(++number);}static void *start_routine(void *args){Thread *_this = static_cast<Thread *>(args);void *ret = _this->run(_this->args_);return ret;}void *run(void *arg){return func_(arg);}void start(func_t func, void *args){func_ = func;args_ = args;int n = pthread_create(&tid_, nullptr, start_routine, this);assert(n == 0);(void)n;}void join(){int n = pthread_join(tid_, nullptr);assert(n == 0);(void)n;}std::string GetTaskName(){return name_;}~Thread() {}private:std::string name_; // 线程名pthread_t tid_;    // 线程idfunc_t func_;      // 线程调用的函数void *args_;       // 线程调用函数的参数
};
int Thread::number = 0;
/* LockGuard.hpp RAII风格的互斥锁 */
#pragma once#include <pthread.h>class Mutex
{
public:Mutex(pthread_mutex_t *lock_p = nullptr) : _lock_p(lock_p) {} // 构造函数void lock() // 加锁{if (_lock_p)pthread_mutex_lock(_lock_p);}void unlock() // 解锁{if (_lock_p)pthread_mutex_unlock(_lock_p);}~Mutex() {} // 析构函数private:pthread_mutex_t *_lock_p;
};class LockGuard // RAII风格的锁的实现
{
public:LockGuard(pthread_mutex_t *mutex) : _mutex(mutex) // 构造函数{_mutex.lock(); // 在构造函数中加锁}~LockGuard() // 析构函数{_mutex.unlock(); // 在析构函数中解锁}private:Mutex _mutex;
};
/* Task.hpp 测试用到的测试任务 */
#pragma once#include <string>
#include <iostream>
#include <functional>static std::string oper = "+-*/%";class CalTask
{
public:using func_t = std::function<int(int, int, char)>;public:CalTask() {}CalTask(int x, int y, char op, func_t func): _x(x), _y(y), _op(op), _callback(func){}std::string operator()(){int result = _callback(_x, _y, _op);char buffer[64];snprintf(buffer, sizeof buffer, "%d %c %d = %d", _x, _op, _y, result);return buffer;}std::string toTaskString(){char buffer[64];snprintf(buffer, sizeof buffer, "%d %c %d = ?", _x, _op, _y);return buffer;}private:int _x;int _y;char _op;func_t _callback;
};
class SaveTask
{typedef std::function<void(const std::string)> func_t;public:SaveTask() {}SaveTask(const std::string &msg, func_t func): _msg(msg), _func(func){}void operator()(){_func(_msg);}private:std::string _msg;func_t _func;
};
int mymath(int a, int b, char op)
{int ans = 0;switch (op){case '+':ans = a + b;break;case '-':ans = a - b;break;case '*':ans = a * b;break;case '/':{if (b == 0){std::cerr << "div zero error!" << std::endl;ans = -1;}elseans = a / b;}break;case '%':{if (b == 0){std::cerr << "mod zero error!" << std::endl;ans = -1;}elseans = a % b;}break;default:break;}return ans;
}
void Save(const std::string &msg)
{FILE *fp = fopen("./log.txt", "a+");if (fp == NULL){std::cerr << "open file error" << std::endl;return;}fputs(msg.c_str(), fp);fputs("\n", fp);fclose(fp);
}

测试代码:

/* main.cc */
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <string>
#include <memory>
#include <unistd.h>int main()
{// 创建线程用于等待输入,执行任务std::unique_ptr<ThreadPool<CalTask>> tp(new ThreadPool<CalTask>());tp->run();// 主线程用于输入任务int x, y;char op;while (true){std::cout << "请输入数据1# ";std::cin >> x;std::cout << "请输入数据2# ";std::cin >> y;std::cout << "请输入你要进行的运算#";std::cin >> op;CalTask t(x, y, op, mymath);tp->push(t);sleep(1);}return 0;
}

运行结果:

image-20240208212419101

2. 线程安全的单例模式

关于设计模式和单例模式的讲解,我们在C++专栏中已经有过了解,感兴趣的可以自行探究,这里附上链接【C++】特殊类设计

我们知道单例模式的设计有饿汉模式懒汉模式两种,饿汉模式的实现会拖慢启动时间,所以这里我们采用懒汉模式来实现我们刚刚创建的线程池。

在一个进程中,有一个线程池即可,所以这里我们的线程池要改写成单例模式的,我们使用懒汉模式来改写

/* 懒汉模式的实现 */
// 头文件 ...// .../* 线程池类的实现 */
template <class T>
class ThreadPool
{
public:// ...
public: // 给handleTask调用的外部接口// ...
public:                               // 需要暴露给外部的接口// ...static ThreadPool<T> *getInstance(){if(nullptr == tp){std::lock_guard<std::mutex> lck(_singletonlock);if(nullptr == tp){tp = new ThreadPool<T> ();}}return tp;}
private: // 单例模式需要私有化的接口ThreadPool(const int &num = gnum) // 构造函数,初始化互斥量和条件变量,构建指定个数的Thread对象{pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);for (int i = 0; i < num; ++i){_threads.push_back(new Thread());}}//delete拷贝构造和析构函数ThreadPool(const ThreadPool<T> &) = delete;ThreadPool<T> *operator=(const ThreadPool<T> &) = delete;private:std::vector<Thread *> _threads; // 保存所有线程对象的指针std::queue<T> _task_queue;      // 需要被分配的任务队列pthread_mutex_t _mutex;         // 任务队列需要被互斥的访问pthread_cond_t _cond;           // 生产任务和消费任务之间需要进行同步static ThreadPool<T> *tp; // 静态成员,存放ThreadPool指针static std::mutex _singletonlock; // 创建线程安全的单例对象要加的锁
};
template<class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;
template<class T>
std::mutex ThreadPool<T>::_singletonlock;

3. STL、智能指针和线程安全

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

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

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

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

4. 其他常见锁的了解

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

5. 读者写者问题

在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁

image-20240208221244264

读写锁的分析:读写锁一共是两个锁,分别为读锁和写锁,对应着读者和写者。当写者需要写数据时,请求写锁,然后再写数据,此时读者不能读,当写锁不存在时,多个读者可以并发的访问同一数据,提高了效率

读者写者问题和生产者消费者模型的本质区别就是消费者会取走数据,而读者不会取走数据

//初始化读写锁
pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);//销毁读写锁
pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//读加锁
pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//写加锁
pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);//解锁
pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

本章完…

分别为读锁和写锁,对应着读者和写者。当写者需要写数据时,请求写锁,然后再写数据,此时读者不能读,当写锁不存在时,多个读者可以并发的访问同一数据,提高了效率

读者写者问题和生产者消费者模型的本质区别就是消费者会取走数据,而读者不会取走数据

//初始化读写锁
pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);//销毁读写锁
pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//读加锁
pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//写加锁
pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);//解锁
pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

本章完…

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

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

相关文章

ONLYOFFICE文档8.0新功能浅探

ONLYOFFICE文档8.0新功能浅探 上个月末这个月初的几天&#xff0c;ONLYOFFICE版本更新了&#xff01;更新到了一个比较整的大的版本号&#xff0c;8.0版本&#xff0c;看来这个生产力工具的升级速度基本上能保持每年两个版本号的速度&#xff0c;还是很快的&#xff0c;一般来…

【stomp实战】websocket原理解析与简单使用

一、WebSocket 原理 WebSocket是HTML5提供的一种浏览器与服务器进行全双工通讯的网络技术&#xff0c;属于应用层协议。它基于TCP传输协议&#xff0c;并复用HTTP的握手通道。浏览器和服务器只需要完成一次握手&#xff0c;两者之间就直接可以创建持久性的连接&#xff0c; 并…

多线程JUC:等待唤醒机制(生产者消费者模式)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位大四、研0学生&#xff0c;正在努力准备大四暑假的实习 &#x1f30c;上期文章&#xff1a;多线程&JUC&#xff1a;解决线程安全问题——synchronized同步代码块、Lock锁 &#x1f4da;订阅专栏&#xff1a;多线程&am…

Kubernetes实战(二十七)-HPA实战

1 HPA简介 HPA 全称是 Horizontal Pod Autoscaler&#xff0c;用于POD 水平自动伸缩&#xff0c; HPA 可以 基于 POD CPU 利用率对 deployment 中的 pod 数量进行自动扩缩容&#xff08;除了 CPU 也可以基于自定义的指标进行自动扩缩容&#xff09;。pod 自动缩放不适用于无法…

NGINX upstream、stream、四/七层负载均衡以及案例示例

文章目录 前言1. 四/七层负载均衡1.1 开放式系统互联模型 —— OSI1.2 四/七层负载均衡 2. Nginx七层负载均衡2.1 upstream指令2.2 server指令和负载均衡状态与策略2.2.1 负载均衡状态2.2.2 负载均衡策略 2.3 案例 3. Nginx四层负载均衡的指令3.1 stream3.2 upstream指令3.3 四…

深入理解ES的倒排索引

目录 数据写入过程 词项字典 term dictionary 倒排表 posting list FOR算法 RBM算法 ArrayContainer BitMapContainer 词项索引 term index 在Elasticsearch中&#xff0c;倒排索引的设计无疑是惊为天人的&#xff0c;下面看下倒排索引的结构。 倒排索引分为词项索引【…

JS中常用占位符使用方法详解_ |%s|%d|%f|%o|%O|%c|

在 JavaScript 中&#xff0c;%s 是一种字符串格式化占位符&#xff0c;用于将字符串插入到另一个字符串中的指定位置。这种方法基于 C 语言的 printf() 函数&#xff0c;但在 JavaScript 中有一些变化。 在 JavaScript 中&#xff0c;%s 可以接受任何类型的值&#xff0c;并将…

上市公司人工智能转型指数及55个工具变量汇总数据集(2024.2月更新)

一、“智能化转型”发文趋势和主题分布 二、数据来源 上市公司年报、官网&#xff0c;中国知网及各期刊官网等三、时间跨度 工具变量&#xff1a;2022-2024年&#xff1b; 上市公司人工智能转型指数&#xff1a;2007-2021年四、数据范围 中国A股上市公司五、数据展示 序号…

一键部署自动化运维工具spug

简介 Spug是面向中小型企业设计的轻量级无Agent的自动化运维平台&#xff0c;整合了主机管理、主机批量执行、主机在线终端、应用发布部署、在线任务计划、配置中心、监控、报警等一系列功能。 部署 1.创建目录 mkdir -p /opt/spug/{mysql,service,repos} 2.进入目录 cd /o…

Modern C++ 内存篇1 - allocator

1. 前言 从今天起我们开始内存相关的话题&#xff0c;内存是个很大的话题&#xff0c;一时不知从何说起。内存离不开allocator&#xff0c;我们就从allocator开始吧。allocator目前有两种&#xff1a;std::allocator, std::pmr::polymorphic_allocator&#xff0c;各有优缺点。…

Vue源码系列讲解——虚拟DOM篇【二】(Vue中的DOM-Diff)

目录 1. 前言 2. patch 3. 创建节点 4. 删除节点 5. 更新节点 6. 总结 1. 前言 在上一篇文章介绍VNode的时候我们说了&#xff0c;VNode最大的用途就是在数据变化前后生成真实DOM对应的虚拟DOM节点&#xff0c;然后就可以对比新旧两份VNode&#xff0c;找出差异所在&…

docker 基于容器创建本地web容器化镜像

一、docker 基于容器创建本地web容器化镜像 1、启动指定buysbox 镜像 docker run --name b1 -it busybox:latest 2、创建目录&#xff0c;并创建html mkdir -p /data/html vi index.html 内容自定义例如&#xff1a;<h1>welcome to busybox<h1> 3、新增窗口&am…

ubuntu22.04 安装部署05:禁用默认显卡驱动

一、相关文章 ubuntu22.04安装部署03&#xff1a; 设置root密码-CSDN博客 《ubuntu22.04装部署01&#xff1a;禁用内核更新》 《ubuntu22.04装部署02&#xff1a;禁用显卡更新》 二、场景说明 Ubuntu22.04 默认显卡驱动&#xff0c;如果安装cuda&#xff0c;需要单独安装显…

Android开发 button 按钮点击两次 响应onclick方法

问题 Android开发 button 按钮点击两次 响应onclick方法 详细问题 笔者xml代码 <!-- 一个按钮 --> <Button android:id"id/button1" android:layout_width"wrap_conten…

Conda历史版本下载地址和python对应关系

一、前言 因为Conda安装版本问题&#xff0c;带来了很多问题&#xff0c;虽然不能直接确定二者之间的关系&#xff0c;但是安装指定版本的conda,确实是一个比较好的方法。特此记忆。 二、下载地址 下载最新版本&#xff1a;Free Download | Anaconda 下载历史版本&#xff…

Xcode配置GLFW GLAD (MAC)

这里的GLFW用的是静态链接 博主反复修改&#xff0c;实在是没能找到为什么用动态会出现线程报错 下载GLAD:版本我一般是选倒数第二新&#xff0c;profile记得选core 点击GENRATE 点glad.zip获得下载 下载GLFW 点击download 最后&#xff0c;将两个文件都放到项目里面去 打开…

形态学操作之开操作与闭操作的python实现——数字图像处理

原理 图像处理中的开操作&#xff08;Opening&#xff09;和闭操作&#xff08;Closing&#xff09;是形态学&#xff08;Morphological&#xff09;操作的两个基本类型&#xff0c;它们都是基于膨胀&#xff08;Dilation&#xff09;和腐蚀&#xff08;Erosion&#xff09;操…

基于PHP的学生管理系统

前言 基于PHP的学生管理系统&#xff1b; 实现 登录、注册、学生信息、修改学生、删除学生、查询学生、添加学生等功能 &#xff1b; 环境准备 开发平台&#xff1a;PhpStrom2022.1.2 、Phpstudy_pro 数据库&#xff1a;MySQL5.7.26 技术架构 Bootstrap PHP7.3.4html5css3 项目…

系统架构21 - 统一建模语言UML(下)

UML图 UML中的图分类作用 视图用例视图逻辑视图进程视图实现视图部署视图 UML中的图 “图”是一组元素的图形表示&#xff0c;大多数情况下把图画成顶点&#xff08;代表事物&#xff09;和弧&#xff08;代表关系&#xff09;的连通图。为了对系统进行可视化&#xff0c;可以…

运维高级篇-分库分表(拆分策略详解)

分库分表 介绍 问题分析 随着互联网及移动互联网的发展&#xff0c;应用系统的数据量也是成指数式增长&#xff0c;若采用单数据库进行数据存 储&#xff0c;存在以下性能瓶颈&#xff1a; IO瓶颈&#xff1a;热点数据太多&#xff0c;数据库缓存不足&#xff0c;产生大量磁盘…