【多线程】补充内容 {线程池;线程安全的单例模式;STL容器、智能指针的线程安全;其他常见的各种锁:自旋锁、读写锁}

一、线程池

1.1 概念

线程池一种线程使用模式:

线程过多会带来调度开销,进而影响缓存局部性和整体性能。

而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务:(线程池的优点)

  • 这避免了在处理短时间任务时创建与销毁线程的代价。
  • 线程池不仅能够保证内核的充分利用,还能防止过分调度。

注意:可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景:

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

线程池使用:

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

1.2 实现

1.2.1 封装线程对象thread + RAII自动加锁解锁

#pragma once
#include <iostream>
#include <pthread.h>
#include <string>
#include <functional>
#include "logmessage.hpp"namespace zty
{typedef void *(*func_t)(void *);//1.封装线程对象class thread{pthread_t _tid;func_t _callback = nullptr;void *_args = nullptr;public:thread() {}thread(func_t callback, void *args): _callback(callback),_args(args){pthread_create(&_tid, nullptr, _callback, _args);}thread(const thread &other) = delete;thread &operator=(const thread &other) = delete;void run(func_t callback, void *args){_callback = callback;_args = args;pthread_create(&_tid, nullptr, _callback, _args);// printf("[%d] run\n", _tid%10000);LogMessage(DEBUG, "[%d] run", _tid%10000);}void join(){// printf("[%d] join\n", _tid%10000);LogMessage(DEBUG, "[%d] join", _tid%10000);pthread_join(_tid, nullptr);}pthread_t get_id(){return _tid;}};//2. RAII自动加锁解锁class lock_guard{pthread_mutex_t &_pmtx;public:lock_guard(pthread_mutex_t &pmtx): _pmtx(pmtx){pthread_mutex_lock(&_pmtx);}~lock_guard(){pthread_mutex_unlock(&_pmtx);}};
}

1.2.2 线程池的实现

下面进行实现一个简单的线程池,线程池中提供了一个任务队列,以及若干个线程(多线程)
img

  • 线程池中的多个线程负责从任务队列当中拿任务,并将拿到的任务进行处理,没有任务就阻塞等待。
  • 线程池对外提供一个push接口,用于让外部线程能够将任务push到任务队列当中

实现代码如下:(在堆区创建的单例懒汉模式)

#pragma once
#include "lockguard.hpp"
#include "thread.hpp"
#include "task.hpp"
#include <vector>
#include <queue>
#include <pthread.h>
#include <unistd.h>namespace zty
{const int THREAD_NUM = 3;template <class T>class thread_pool{static thread_pool *s_ins;int _thread_num;std::vector<zty::thread> _threads;std::queue<T> _tasks;static pthread_mutex_t _mtx;static pthread_cond_t _cond;bool _terminate = false; // 结束标志thread_pool(int thread_num = THREAD_NUM): _thread_num(thread_num),_threads(_thread_num){}~thread_pool(){_terminate = true;              // 设置结束标志pthread_cond_broadcast(&_cond); // 唤醒所有等待条件变量的线程for (auto &e : _threads)        // 回收所有子线程{e.join();}}thread_pool(const thread_pool &other) = delete;thread_pool &operator= (const thread_pool &other) = delete;struct GC{~GC(){if (s_ins != nullptr){delete s_ins;s_ins = nullptr;}}};public:static thread_pool &GetInstance(int num = THREAD_NUM){if (s_ins == nullptr){zty::lock_guard lock(_mtx);if (s_ins == nullptr){s_ins = new thread_pool(num);}}return *s_ins;}void push(const T &task){zty::lock_guard lock(_mtx);_tasks.push(task);pthread_cond_signal(&_cond);}bool pop(T &out){zty::lock_guard lock(_mtx);while (_tasks.empty()){pthread_cond_wait(&_cond, &_mtx);if (_terminate)return false;}out = _tasks.front();_tasks.pop();return true;}void run(){for (auto &e : _threads){e.run(routine, this);}}//pthread_creat函数要求的线程入口点函数的参数只有一个void*,不能有this指针。static void *routine(void *args) {thread_pool *self = (thread_pool *)args; // self实际就是this指针T task;while (!self->_terminate){bool ret = self->pop(task);if (ret) // 判断self->pop是否获取到任务了{LogMessage(NORMAL, "[%d] %d%c%d=%d", pthread_self() % 10000, task._l, task._op, task._r, task());sleep(1);}}return (void *)0;}};template <class T>thread_pool<T> *thread_pool<T>::s_ins = nullptr;template <class T>pthread_mutex_t thread_pool<T>::_mtx = PTHREAD_MUTEX_INITIALIZER;template <class T>pthread_cond_t thread_pool<T>::_mtx = PTHREAD_CONDITION_INITIALIZER;template <class T>typename thread_pool<T>::GC thread_pool<T>::s_gc;
}

test.cc

#include "thread.hpp"
#include "lockguard.hpp"
#include "task.hpp"
#include "threadpool.hpp"
#include "logmessage.hpp"
#include <thread>
#include <unistd.h>
#include <ctime>
#include <cstdlib>
using namespace std;int main()
{srand((unsigned int)time(nullptr));// zty::thread_pool<zty::Task> tp(5);zty::thread_pool<zty::Task> &tp = zty::thread_pool<zty::Task>::GetInstance();tp.run();char ops[] = {'+', '-', '*', '/' ,'%'};int cnt = 5;while(cnt--){int l = rand()%100;int r = rand()%100;char op = ops[rand()%5];zty::Task task(l, r, op);// printf("main_thread: %d%c%d=?\n", task._l, task._op, task._r);LogMessage(NORMAL, "main_thread: %d%c%d=?", task._l, task._op, task._r);tp.push(task);sleep(1);}return 0;
}

logmessage.hpp

#pragma once
#include <cstdarg>
#include <cstdio>
#include <ctime>
#include <cstring>enum LEVEL
{DEBUG,NORMAL,WARNING,ERROR,FATAL
};const char *lvtable[] = {"DEBUG", "NORMAL", "WARNING", "ERROR", "FATAL"};
const char *filepath = "./log.txt";void LogMessage(LEVEL level, const char *format, ...)
{
#ifndef DEBUGSHOWif (level == DEBUG)return;
#endif char buffer[1024];time_t timestamp;time(&timestamp);tm *timeinfo = localtime(&timestamp);snprintf(buffer, sizeof(buffer), "[%d/%d/%d]%s: ", timeinfo->tm_year + 1900, timeinfo->tm_mon, timeinfo->tm_mday, lvtable[level]);va_list ap;va_start(ap, format);vsnprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), format, ap);va_end(ap);printf("%s\n", buffer);FILE *fp = fopen(filepath, "a");fprintf(fp, "%s\n", buffer);
}

二、线程安全的单例模式

这里在C++篇章已经谈过,这里不再赘述,链接::【C++】特殊类设计 {不能被拷贝的类;只能在堆上创建的类;只能在栈上创建的类;不能被继承的类;单例模式:懒汉模式,饿汉模式}-CSDN博客

注意事项:

  1. 单例模式有两种实现模式:饿汉模式(启动时实例化对象)和懒汉模式(在任意程序模块第一次访问单例时实例化对象)。
  2. 单例对象可以在堆区创建,也可以在静态区创建
  3. 在堆区创建的懒汉单例,获取单例指针时需要双检查加锁:双重 if 判定, 避免不必要的锁竞争
  4. 在堆区创建单例时,包含一个静态的内部类对象,该对象析构时会顺便析构单例,自动释放。

三、STL容器、智能指针的线程安全

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

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

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

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

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

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

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


四、其他常见的各种锁

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

4.1 pthread自旋锁

特性

  • 自旋锁是一种基于忙等待的锁,用于保护共享资源的访问。当一个线程尝试获取自旋锁时,如果锁已经被其他线程占用,则该线程会一直循环等待,直到锁被释放。这种等待的过程称为自旋
  • 不同于互斥锁,线程竞争自旋锁失败后不会被阻塞挂起,而是会进行自旋检测,直到获取锁
  • 无论是互斥锁的挂起等待还是自旋锁的轮询检测都是由pthread库完成的,我们不需要自行操作。因此自旋锁的使用方法和互斥锁相同。

使用场景

  • 自旋锁不会引起线程的上下文切换,而互斥锁(如Mutex)可能会引起线程的上下文切换,因此在一些锁的持有时间很短的场景下,使用自旋锁可以减少上下文切换的开销。
  • 自旋锁适用于等待时间短的情况,在等待时间短的情况下,自旋锁的效率较高。如果等待时间较长,自旋锁可能会消耗大量的CPU资源。

相关接口

#include <pthread.h>pthread_spinlock_t spinlock; // 定义一个自旋锁
pthread_spin_init(&spinlock, 0); // 初始化自旋锁
pthread_spin_lock(&spinlock); // 获取自旋锁 
pthread_spin_unlock(&spinlock); // 释放自旋锁
pthread_spin_destroy(&spinlock); // 销毁自旋锁

应用层实现

//可以使用while循环+pthread_mutex_trylock在应用层实现一个自旋锁
while(pthread_mutex_trylock(&lock) == EBUSY);
//访问临界资源
//......
pthread_mutex_unlock(&lock);

4.2 pthread读写锁

特性

读写锁(Read-Write Lock)是一种特殊的锁机制,它允许多个线程同时获得读访问权限,但同一时间只有一个线程可以获得写访问权限。读写锁适用于读操作远远超过写操作的场景,可以提高并发性能。

使用场景

读写锁适用于读操作频繁而写操作较少的场景。如果读操作和写操作的数量相差不大,或者写操作频繁,可能会导致读写锁的效率不如互斥锁。

读者写者模型的特点

321原则(便于记忆)

  • 三种关系: 写者和写者(互斥关系)、读者和读者(没有关系)、写者和读者(互斥关系、同步关系)。
  • 两种角色: 写者和读者(通常由线程承担)
  • 一个交易场所: 通常指的是内存中的一段共享缓冲区(共享资源)

读者写者模型 VS 生产消费模型的本质区别:消费者会拿走数据,读者不会(读者只会对数据进行拷贝、访问…)

相关接口

#include <pthread.h>//定义一个读写锁
pthread_rwlock_t xxx
//初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t*restrict attr);
//销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//加锁和解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//读锁加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//写锁加锁int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//解锁共用

示例代码

下面是一个使用pthread库中读写锁的简单示例:

#include <pthread.h>
#include <stdio.h>// 定义一个全局读写锁
pthread_rwlock_t rwlock;int shared_data = 0;void* reader(void* arg) {int tid = *((int*)arg);while (1) {pthread_rwlock_rdlock(&rwlock); // 获取读锁printf("Reader %d: Shared data = %d\n", tid, shared_data);pthread_rwlock_unlock(&rwlock); // 释放读锁// 模拟读操作完成后的延迟usleep(1000000);}pthread_exit(NULL);
}void* writer(void* arg) {int tid = *((int*)arg);while (1) {pthread_rwlock_wrlock(&rwlock); // 获取写锁shared_data++; // 修改共享数据printf("Writer %d: Modified shared data to %d\n", tid, shared_data);pthread_rwlock_unlock(&rwlock); // 释放写锁// 模拟写操作完成后的延迟usleep(2000000);}pthread_exit(NULL);
}int main() {pthread_t reader1, reader2, writer1;int tid1 = 1, tid2 = 2, tid3 = 3;pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁// 创建两个读线程和一个写线程pthread_create(&reader1, NULL, reader, &tid1);pthread_create(&reader2, NULL, reader, &tid2);pthread_create(&writer1, NULL, writer, &tid3);// 主线程等待读写线程结束pthread_join(reader1, NULL);pthread_join(reader2, NULL);pthread_join(writer1, NULL);pthread_rwlock_destroy(&rwlock); // 销毁读写锁return 0;
}

需要注意的是,读写锁采用共享/排他的锁控制策略。当有读者线程持有读锁时,其他读者线程可以继续获取读锁;但当有写者线程持有写锁时,其他任何读者线程或写者线程都无法获取读或写锁。

读写锁的实现原理:

在这里插入图片描述

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

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

相关文章

git的配置使用

第三周 Tursday 早 git日志的安装使用 [rootweb ~]# yum -y install git.x86_64 //安装软件包 [rootweb ~]# rpm -ql git //查看git的包 ​ [rootweb ~]# mkdir /yy000 //创建新目录 [rootweb ~]# cd /yy000/ [rootweb yy000]# git init //将当前目录做为仓库…

OpenAI开放GPT-4o语音模式测试;黄仁勋与扎克伯格谈AI未来

&#x1f989; AI新闻 &#x1f680; OpenAI开放GPT-4o语音模式测试 摘要&#xff1a;7月30日&#xff0c;OpenAI宣布向部分ChatGPT Plus用户开放GPT-4o语音模式&#xff08;Alpha版本&#xff09;&#xff0c;并计划秋季推广至所有订阅用户。GPT-4o具备快速反应、近乎无缝对…

【信创】samba的命令行使用 _ 统信 _ 麒麟 _ 中科方德

原文链接&#xff1a;【信创】samba的命令行使用 | 统信 | 麒麟 | 中科方德 Hello&#xff0c;大家好啊&#xff01;今天给大家带来一篇关于在信创终端操作系统上使用Samba命令操作的文章。Samba是一种用于实现文件和打印共享的免费软件&#xff0c;它允许不同操作系统&#xf…

基于springboot+vue+uniapp的校园快递平台小程序

开发语言&#xff1a;Java框架&#xff1a;springbootuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#…

Android经典面试题之实战经验分享:如何简单实现App的前后台监听判断

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 在Android中判断一个应用是否处于前台或后台&#xff0c;可以使用ActivityLifecycleCallbacks 和 ProcessLifecycleOwner。在Kotlin中&#xff…

Labtoolstcl 44-27] No hardware targets exist on the server [localhost:3121]

发现问题 换了台电脑重新装了vivado17.2版本的软件&#xff0c;编译好程序后准备烧程序&#xff0c;板子上电以及连接线都检查过了没问题&#xff0c;但是就是检测不到芯片&#xff1b; 过程 一、网上查说是驱动没装好&#xff0c;按照其他博主的方法运行了digilent目录下的…

【CAN通讯系列6】 CAN总线定义

在前面两篇文章&#xff1a; 【CAN通讯系列3】如何学习CAN通讯&#xff1f; 【CAN通讯系列4】CAN通讯如何传递信号&#xff1f; 我们已经解决了一个控制器的多个信号通过怎样的形式传递给另一个控制器&#xff0c;即通过CAN数据帧&#xff0c;包含报文ID&#xff0c;数据长度和…

适合新手小白入手前先看的开放式耳机选购指南

对于新手小白来说&#xff0c;选择一款开放式耳机其实一般都会不知道从哪个方面考虑入手。 所以我可以与你分享一些选购经验&#xff0c;让你更能知道需要什么样的开放式耳机。 确定需求&#xff1a; 明确你购买开放式耳机的主要用途&#xff0c;比如听音乐、看电影、游戏或…

书生大模型训练营 - 练习一

最近想了解一下大模型&#xff0c;查看了《2024大模型典型示范应用》文档&#xff0c;发现有公司使用的是书生大模型&#xff0c;正好发现他们有训练营&#xff0c;此文章记录的大模型作业。 一、各种链接 书生大模型官网&#xff1a;https://internlm.intern-ai.org.cn/ 进训…

实验2-5-1 求排列数

本题要求实现一个计算阶乘的简单函数&#xff0c;使得可以利用该函数&#xff0c;根据公式 算出从n个不同元素中取出m个元素&#xff08;0<m≤n&#xff09;的排列数。 函数接口定义&#xff1a; double fact( int n );其中n是用户传入的参数&#xff0c;函数返回n的阶乘。…

什么是跳板机

1、什么是跳板机 跳板机&#xff08;Jump Server&#xff09;是一种网络安全设备&#xff0c;用于在不直接连接到目标服务器的情况下&#xff0c;提供一个中间的访问点。它允许管理员或用户通过一个受控的安全通道访问内部网络中的其他服务器、设备或资源。 跳板机的主要功能…

芋道源码yudao-cloud 二开笔记(Editor富文本本地图片上传报错问题)

&#xff1a; 于是找到富文本的组件代码Editor.vue&#xff0c;检查一下上传的接口地址和token有没有传&#xff0c;如下图&#xff1a; 都没有问题&#xff0c;但还是报错&#xff0c;所以试试自定义上传的方法&#xff1a; // 导入上传文件的接口 import * as FileApi from …

一步步揭秘:浏览器输入URL后的那些事儿

Hello大家好,我是小米,一个热爱分享技术的IT达人。今天我们一起来聊聊一个大家每天都会用到但可能不太了解的过程:浏览器输入URL之后发生了什么。这是一个从用户输入到浏览器显示页面的完整过程,涉及到很多底层的网络基础知识。今天我们就一起来探究这个神秘的过程! DNS解…

Golang基础常识性知识面试中常见的六大陷阱及应对技巧

一、nil slice & empty slice 1、nil切片与空切片底层 nil切片&#xff1a;var nilSlice [] string nil slice的长度len和容量cap都是0 nil slicenil nil slice的pointer是nil 空切片&#xff1a;emptySlice0 : make([]int,0) empty slice的长度是0&#xff0c;容量是由…

【Story】《程序员面试的“八股文”辩论:技术基础与实际能力的博弈》

目录 程序员面试中的“八股文”&#xff1a;助力还是阻力&#xff1f;1. “八股文”的背景与定义1.1 “八股文”的起源1.2 “八股文”的常见类型 2. “八股文”的作用分析2.1 理论基础的评价2.1.1 助力2.1.2 阻力 3. 实际工作能力的考察3.1 助力3.2 阻力 4. 面试中的背题能力4.…

C# 表达式树的简介与说明

文章目录 1. 表达式树是什么&#xff1f;2. 表达式树的基本组成3. 构建表达式树的步骤4. 表达式树的使用场景5. 示例代码6. 总结 在 C# 编程中&#xff0c;表达式树&#xff08;Expression Tree&#xff09;是一个强大的概念&#xff0c;它允许我们以代码的形式表示运行时的代码…

二分算法及其公式

二分查找 二分查找是大多数人第一个接触到的算法&#xff0c;很多人都认为只有有序的数组可以使用二分查找&#xff0c;但这种思想其实是错误的&#xff0c;二分查找是可以用于拥有二段性的数组&#xff0c;而且二分算法是由模板做参考的&#xff0c;所以只要掌握就可以解决大…

机器学习课程学习周报六

机器学习课程学习周报六 文章目录 机器学习课程学习周报六摘要Abstract一、机器学习部分1.1 循环神经网络概述1.2 循环神经网络架构1.2.1 深层循环神经网络1.2.2 Elman网络和Jordan网络1.2.3 双向循环神经网络 1.3 长短期记忆网络1.4 LSTM原理1.5 RNN的学习方式1.6 RNN中的梯度…

BUG解决(vue3+echart报错):Cannot read properties of undefined (reading ‘type‘)

这是 vue3echart5 遇到的报错&#xff1a;Cannot read properties of undefined (reading ‘type‘) 这个问题需要搞清楚两个关键方法&#xff1a; toRaw&#xff1a; 作用&#xff1a;将一个由reactive生成的响应式对象转为普通对象。 使用场景&#xff1a; 用于读取响应式…

word修改一处全文都变了

在word中修改一处文字&#xff0c;全文都变了&#xff0c;还得重新排版。来来回回、反反复复特别麻烦。主要是因为word的文档用WPS打开&#xff0c;WPS自动做了更改。我们将word中的文字的样式取消自动更新就可以了。 右键“正文1”&#xff0c;选择“修改” 将子等更新的√&am…