【Linux】线程池 | 自旋锁 | 读写锁

文章目录

  • 一.Linux线程池
    • 1.线程池的概念
    • 2.线程池的优点
    • 3.线程池的应用场景
    • 4.线程池的实现
  • 二.其他常见的锁
    • 1.STL、智能指针和线程安全
    • 2.其他常见的锁
  • 三.读者写者问题
    • 1.读者写者模型
    • 2.读写锁

一.Linux线程池

1.线程池的概念

线程池是一种线程使用模式。

线程过多会带来调度开销,进而影响缓存局部和整体性能,而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。

2.线程池的优点

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

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

3.线程池的应用场景

线程池常见的应用场景如下:

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。

相关解释:

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

4.线程池的实现

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

在这里插入图片描述

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

线程池的代码如下:

#pragma once#include <iostream>
#include <unistd.h>
#include <queue>
#include <pthread.h>#define NUM 5//线程池
template<class T>
class ThreadPool
{
private:bool IsEmpty(){return _task_queue.size() == 0;}void LockQueue(){pthread_mutex_lock(&_mutex);}void UnLockQueue(){pthread_mutex_unlock(&_mutex);}void Wait(){pthread_cond_wait(&_cond, &_mutex);}void WakeUp(){pthread_cond_signal(&_cond);}
public:ThreadPool(int num = NUM): _thread_num(num){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}//线程池中线程的执行例程static void* Routine(void* arg){pthread_detach(pthread_self());ThreadPool* self = (ThreadPool*)arg;//不断从任务队列获取任务进行处理while (true){self->LockQueue();while (self->IsEmpty()){self->Wait();}T task;self->Pop(task);self->UnLockQueue();task.Run(); //处理任务}}void ThreadPoolInit(){pthread_t tid;for (int i = 0; i < _thread_num; i++){pthread_create(&tid, nullptr, Routine, this); //注意参数传入this指针}}//往任务队列塞任务(主线程调用)void Push(const T& task){LockQueue();_task_queue.push(task);UnLockQueue();WakeUp();}//从任务队列获取任务(线程池中的线程调用)void Pop(T& task){task = _task_queue.front();_task_queue.pop();}
private:std::queue<T> _task_queue; //任务队列int _thread_num; //线程池中线程的数量pthread_mutex_t _mutex;pthread_cond_t _cond;
};

为什么线程池中需要有互斥锁和条件变量?

线程池中的任务队列是会被多个执行流同时访问的临界资源,因此我们需要引入互斥锁对任务队列进行保护。

线程池当中的线程要从任务队列里拿任务,前提条件是任务队列中必须要有任务,因此线程池当中的线程在拿任务之前,需要先判断任务队列当中是否有任务,若此时任务队列为空,那么该线程应该进行等待,直到任务队列中有任务时再将其唤醒,因此我们需要引入条件变量。

当外部线程向任务队列中Push一个任务后,此时可能有线程正处于等待状态,因此在新增任务后需要唤醒在条件变量下等待的线程。

注意:

  • 当某线程被唤醒时,其可能是被异常或是伪唤醒,或者是一些广播类的唤醒线程操作而导致所有线程被唤醒,使得在被唤醒的若干线程中,只有个别线程能拿到任务。此时应该让被唤醒的线程再次判断是否满足被唤醒条件,所以在判断任务队列是否为空时,应该使用while进行判断,而不是if。
  • pthread_cond_broadcast函数的作用是唤醒条件变量下的所有线程,而外部可能只Push了一个任务,我们却把全部在等待的线程都唤醒了,此时这些线程就都会去任务队列获取任务,但最终只有一个线程能得到任务。一瞬间唤醒大量的线程可能会导致系统震荡,这叫做惊群效应。因此在唤醒线程时最好使用pthread_cond_signal函数唤醒正在等待的一个线程即可。
  • 当线程从任务队列中拿到任务后,该任务就已经属于当前线程了,与其他线程已经没有关系了,因此应该在解锁之后再进行处理任务,而不是在解锁之前进行。因为处理任务的过程可能会耗费一定的时间,所以我们不要将其放到临界区当中。
  • 如果将处理任务的过程放到临界区当中,那么当某一线程从任务队列拿到任务后,其他线程还需要等待该线程将任务处理完后,才有机会进入临界区。此时虽然是线程池,但最终我们可能并没有让多线程并行的执行起来。

为什么线程池中的线程执行例程需要设置为静态方法?

使用pthread_create函数创建线程时,需要为创建的线程传入一个Routine(执行例程),该Routine只有一个参数类型为void的参数,以及返回类型为void的返回值。

而此时Routine作为类的成员函数,该函数的第一个参数是隐藏的this指针,因此这里的Routine函数,虽然看起来只有一个参数,而实际上它有两个参数,此时直接将该Routine函数作为创建线程时的执行例程是不行的,无法通过编译。

静态成员函数属于类,而不属于某个对象,也就是说静态成员函数是没有隐藏的this指针的,因此我们需要将Routine设置为静态方法,此时Routine函数才真正只有一个参数类型为void*的参数。

但是在静态成员函数内部无法调用非静态成员函数,而我们需要在Routine函数当中调用该类的某些非静态成员函数,比如Pop。因此我们需要在创建线程时,向Routine函数传入的当前对象的this指针,此时我们就能够通过该this指针在Routine函数内部调用非静态成员函数了。

任务类型的设计

我们将线程池进行了模板化,因此线程池当中存储的任务类型可以是任意的,但无论该任务是什么类型的,在该任务类当中都必须包含一个Run方法,当我们处理该类型的任务时只需调用该Run方法即可。

例如,下面我们实现一个计算任务类:

#pragma once#include <iostream>//任务类
class Task
{
public:Task(int x = 0, int y = 0, char op = 0): _x(x), _y(y), _op(op){}~Task(){}//处理任务的方法void Run(){int result = 0;switch (_op){case '+':result = _x + _y;break;case '-':result = _x - _y;break;case '*':result = _x * _y;break;case '/':if (_y == 0){std::cerr << "Error: div zero!" << std::endl;return;}else{result = _x / _y;}break;case '%':if (_y == 0){std::cerr << "Error: mod zero!" << std::endl;return;}else{result = _x % _y;}break;default:std::cerr << "operation error!" << std::endl;return;}std::cout << "thread[" << pthread_self() << "]:" << _x << _op << _y << "=" << result << std::endl;}
private:int _x;int _y;char _op;
};

此时线程池内的线程不断从任务队列拿出任务进行处理,而它们并不需要关心这些任务是哪来的,它们只需要拿到任务后执行对应的Run方法即可。

主线程逻辑

主线程就负责不断向任务队列当中Push任务就行了,此后线程池当中的线程会从任务队列当中获取到这些任务并进行处理。

#include "Task.hpp"
#include "ThreadPool.hpp"int main()
{srand((unsigned int)time(nullptr));ThreadPool<Task>* tp = new ThreadPool<Task>; //线程池tp->ThreadPoolInit(); //初始化线程池当中的线程const char* op = "+-*/%";//不断往任务队列塞计算任务while (true){sleep(1);int x = rand() % 100;int y = rand() % 100;int index = rand() % 5;Task task(x, y, op[index]);tp->Push(task);}return 0;
}

运行代码后一瞬间就有六个线程,其中一个是主线程,另外五个是线程池内处理任务的线程。

在这里插入图片描述

并且我们会发现这五个线程在处理时会呈现出一定的顺序性,因为主线程是每秒Push一个任务,这五个线程只会有一个线程获取到该任务,其他线程都会在等待队列中进行等待,当该线程处理完任务后就会因为任务队列为空而排到等待队列的最后,当主线程再次Push一个任务后会唤醒等待队列首部的一个线程,这个线程处理完任务后又会排到等待队列的最后,因此这五个线程在处理任务时会呈现出一定的顺序性。

在这里插入图片描述

注意: 此后我们如果想让线程池处理其他不同的任务请求时,我们只需要提供一个任务类,在该任务类当中提供对应的任务处理方法就行了。

二.其他常见的锁

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

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

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

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

  • unique_ptr是和资源强关联,只是在当前代码块范围内生效,因此不涉及线程安全问题。
  • 对于 shared_ptr,多个对象需要共有一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候也考虑到了这个问题,就基于原子操作(Compare And Swap(CAS)) 的方式保证 shared_ptr 能够高效原子地操作引用计数。shared_pt 是线程安全的,但不意味着对其管理的资源进行操作是线程安全的,所以对shared_ptr管理的资源进行操作时也可能需要进行加锁保护。

2.其他常见的锁

  • 悲观锁:悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问贡献资源前,先要进行加锁保护。常见的悲观锁有:互斥锁、自旋锁和读写锁等。
  • 乐观锁:乐观锁做事比较乐观,它乐观地认为共享数据不会被其他线程修改,因此不上锁。它的工作方式是:先修改完共享数据,再判断这段时间内有没有发生冲突。如果其他线程没有修改共享数据,那么则操作成功。如果发现其他线程已经修改该共享数据,就放弃本次操作。乐观锁全程并没有加锁,所以它也叫无锁编程。乐观锁主要采取两种方式:版本号机制(Gitee等)和 CAS 操作。乐观锁虽然去除了加锁和解锁的操作,但是一旦发生冲突,重试的成本是很高的,所以只有在冲突概率非常低,且加锁成本非常高的场景下,才考虑使用乐观锁。
  • CAS 操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 自旋锁:使用自旋锁的时候,当多线程发生竞争锁的情况时,加锁失败的线程会忙等待(这里的忙等待可以用 while 循环等待实现),直到它拿到锁。而互斥锁加锁失败后,线程会让出 CPU 资源给其他线程使用,然后该线程会被阻塞挂起。如果临界区代码执行时间过长,自旋的线程会长时间占用 CPU 资源,所以自旋的时间和临界区代码执行的时间是成正比的关系。如果临界区代码执行的时间很短,就不应该使用互斥锁,而应该选用自旋锁。因为互斥锁加锁失败,是需要发生上下文切换的,如果临界区执行的时间比较短,那可能上下文切换的时间会比临界区代码执行的时间还要长。

三.读者写者问题

1.读者写者模型

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

这就需要我们的读者写者模型出场了,读者写者模型其实也是维护321原则:

  • 三种关系:读者与读者、读者与写者、写者与写者。
  • 两种对象:读者和写者。
  • 一个交易场所:需要写入和从中读取的缓冲区。

下面我们来看一下读者写者模型的三种关系:

  • 读者与读者:没有关系
  • 读者与写者:互斥与同步
  • 写者与写者:互斥

那么,为什么在生产者消费者模型中,消费者和消费者是互斥关系,而在读者写者问题中,读者和读者之间没有关系呢?

读者写者模型和生产者消费者模型的最大区别就是:消费者会将数据拿走,而读者不会拿走数据,读者仅仅是对数据做读取,并不会进行任何修改的操作,因此共享资源也不会因为有多个读者来读取而导致数据不一致的问题。

2.读写锁

在读者写者模型中,pthread库为我们提供了 读写锁 来维护其中的同步与互斥关系。读写锁由读锁和写锁两部分构成,如果只读取共享资源用读锁加锁,如果要修改共享资源则用写锁加锁。所以,读写锁适用于能明确区分读操作和写操作的场景。

读写锁的工作原理:

当写锁没有被写线程持有时,多个读线程能够并发地持有读锁,这大大提高了共享资源的访问效率。因为读锁是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。但是,一旦写锁被写进程持有后,读线程获取读锁的操作会被阻塞,而其它写线程的获取写锁的操作也会被阻塞。

伪代码:

// 写者进程/线程执行的函数
void Writer()
{while(true){P(wCountMutex); // 进入临界区if(wCount == 0)P(rMutex); // 当第一个写者进入,如果有读者则阻塞读者wCount++;// 写者计数 + 1V(wCountMutex); // 离开临界区P(wDataMutex); // 写者写操作之间互斥,进入临界区write(); // 写数据V(wDataMutex); // 离开临界区P(wCountMutex); // 进入临界区wCount--; // 写完数据,准备离开if(wCount == 0){V(rMutex);  // 最后一个写者离开了,则唤醒读者}V(wCountMutex); //离开临界区}
}// 读者进程/线程执行的次数
void reader()
{while(TRUE){P(rMutex);P(rCountMutex); // 进入临界区if ( rCount == 0 )P(wDataMutex); // 当第一个读者进入,如果有写者则阻塞写者写操作rCount++;V(rCountMutex); // 离开临界区V(rMutex);read( ); // 读数据P(rCountMutex); // 进入临界区rCount--;if ( rCount == 0 )V(wDataMutex); // 当没有读者了,则唤醒阻塞中写者的写操作V(rCountMutex); // 离开临界区}
}

在这里插入图片描述
初始化:

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);

读者写者问题很明显会存在读者优先还是写者优先的问题,如果是读者优先的话,可能就会带来写者饥饿的问题。而写者优先可以保证写线程不会饿死,但如果一直有写线程获取写锁,那么读者也会被饿死。所以使用读写锁时,需要考虑应用场景。读写锁通常用于数据被读取的频率非常高,而被修改的频率非常低。

:Linux 下的读写锁默认是读者优先的。


本文到此结束,码文不易,还请多多支持哦!!!

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

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

相关文章

网站二级域名怎么部署SSL证书?

二级域名是在主域名下创建的子域名&#xff0c;常用于区分不同功能或部门的网站。随着互联网的发展&#xff0c;越来越多的网站开始采用二级域名来构建更灵活和个性化的网站结构&#xff0c;保护二级域名的数据安全也变得至关重要。为了确保二级域名的安全性&#xff0c;申请SS…

【七:(测试用例)spring boot+testng+xml+mock实现用例管理+数据校验】

目录 1、目录结构的相关类cases类1、添加用户 AddUserTest2、获取用户列表信息 GetUserInfoListTest3、获取用户信息 GetUserInfoTest4、登录测试5、更新用户信息 config类1、报告配置2、用户路径配置 model类utils类 配置配置类SQLMapper.xmlspring boot全局配置databaseConfi…

了解 Elasticsearch 自动生成的文档 _id:重复是一个问题吗?

Elasticsearch 中自动生成的文档 ID 当你在未指定 ID 的情况下对文档建立索引时&#xff0c;Elasticsearch 会自动为该文档生成唯一的 ID。 该 ID 是 Base64 编码的 UUID&#xff0c;由多个部分组成&#xff0c;每个部分都有特定的用途。 ID 生成过程针对索引速度和存储效率进…

2023年中国人力资源咨询发展历程及市场规模前景分析[图]

人力资源咨询是企业借助外部智力资源提高自身管理水平和效率的重要路径&#xff0c;属于管理咨询业的一个重要分支, 一方面&#xff0c;人力资源咨询要为企业提供基础的人力资源外包服务&#xff1b;另一方面&#xff0c;人力资源咨询要为企业提供专业化、职业化现代人力资源管…

用CSS+SVG做一个优雅的环形进度条

开门见山 先上最终效果图&#xff1a;&#xff08;Demo 传送门&#xff09; 其中进度、尺寸、环宽和颜色都可以非常方便地进行控制。 核心原理&#xff1a; 利用两个重叠的圆环形&#xff0c;通过对上层圆环弧长的控制来表示进度&#xff0c;下层圆环则作为辅助&#xff0c;…

开源云财务软件,财务软件源码,永久免费财务软件

纷析云SAAS云财务软件开源版 包含账套、凭证字、科目、期初、币别、账簿、报表、凭证、结账 技术交流群 扫码添加客服进群 商业版地址 纷析云商业版https://f3.fenxi365.com/ 正式环境&#xff0c;可注册账号直接使用或测试 功能对比 功能模块开源版商业版[技术重构]凭证✔…

蜻蜓c影视追剧系统-多个小程序添加说明

多小程序添加设置 蜻蜓c影视追剧 支持多小程序添加&#xff0c;也就是可以管理不同前端的小程序。 此处id 对应前端小程序的mp值 关于添加小程序&#xff1a; 此处有所有填写内容的参考方式&#xff0c;要注意是必须开通了微信支付才可以添加&#xff0c;这里需要添加证书信息…

高精度时间测量(TDC)电路MS1022

MS1022 是一款高精度时间测量电路&#xff0c;内部集成了模拟比 较器、模拟开关、施密特触发器等器件&#xff0c;从而大大简化了外 围电路。同时内部增加了第一波检测功能&#xff0c;使抗干扰能力大 大提高。通过读取第一个回波脉冲的相对宽度&#xff0c;用户可以获 得接…

微信小程序文本横向无缝滚动

背景&#xff1a; 微信小程序中列表宽度不够长&#xff0c;其中某字段显示不完整&#xff0c;因此要使其自动滚动。 &#xff08;最初看网上很多用定时器实现&#xff0c;但他们的案例中都只是一个横幅、用定时器也无所谓。但是我的需求中是一个上下无限滚动的列表&#xff0c;…

离线语音与IoT结合:智能家居发展新增长点

离线语音控制和物联网&#xff08;IoT&#xff09;相结合在家居中具有广泛的应用和许多优势。离线语音控制是指在设备在本地进行语音识别和处理&#xff0c;而不需要依赖云服务器进行处理。IoT是指借助网络&#xff0c;通过手机APP、小程序远程控制家居设备。 启英泰伦基于AI语…

【面试系列】Vue

引言&#xff1a;下面是一些常见的 Vue 面试题和对应的答案 目录 1. Vue 是什么&#xff1f;它有哪些特点&#xff1f;2. Vue 的生命周期有哪些&#xff1f;请简要描述每个生命周期的作用。3. Vue 组件间通信的有哪些方式&#xff1f;4. Vue 的 computed 和 watch 的区别是什么…

python安装gdal

下载whl https://www.lfd.uci.edu/~gohlke/pythonlibs/#gdal 安装 pip install GDAL-3.1.4-cp36-cp36m-win_amd64.whl

iMazing苹果用户手机备份工具 兼容最新的iOS16操作系统

现在距离苹果秋季新品发布会已过去月余&#xff0c;新iPhone 14系列和新版的iOS 16操作系统也如约与我们见面了&#xff0c;相信大家在9月初抢购的iPhone 14也基本到手了&#xff0c;但随之到来的数据资料备份迁移却是一件令人头大的事情&#xff0c;使用官方提供的iTunes软件卡…

easyrecovery2024数据恢复软件最新版本下载

easyrecovery是PC上数据恢复领域相当给力的应用软件之一&#xff0c;它具有操作安全&#xff0c;价格便宜&#xff0c;支持用户自主操作等特点&#xff0c;能支持从各种存储介质恢复删除、格式化或者丢失的文件&#xff0c;从任何存储介质设备上的损坏&#xff0c;删除&#xf…

搭建react项目

一、环境准备 1、安装node 官网下载安装&#xff1a;https://nodejs.org/en 注&#xff1a; npm5.2以后&#xff0c;安装node会自动安装npm和npx 2、安装webpack npm install -g webpack3、安装create-react-app npm install -g create-react-app二、创建react项目 1、初…

DFS(分布式文件系统)与 DFSR(分布式文件系统复制)的区别

DFS&#xff08;分布式文件系统&#xff09;和 DFSR&#xff08;分布式文件系统复制&#xff09;是两种不同的技术&#xff0c;尽管它们在名称上有一些相似之处&#xff0c;但它们的用途和功能有所不同。 DFS&#xff08;分布式文件系统&#xff09; DFS 是一种用于创建和管理…

GEE:为机器学习算法(随机森林、支持矢量机等)加入膨胀/腐蚀特征

作者:CSDN @ _养乐多_ 腐蚀和膨胀是数学形态学图像处理中的两个基本操作,用于修改和分析二值图像(包含只有两个像素值的图像,通常是黑和白)。 腐蚀和膨胀操作可以作为机器学习中的特征变量,用来分类,比如在博客《GEE:随机森林分类教程(样本制作、特征添加、训练、精…

Flink学习---15、FlinkCDC(CDC介绍、案例实操)

星光下的赶路人star的个人主页 未来总是藏在迷雾中让人胆怯&#xff0c;但当你踏入其中&#xff0c;便会云开雾散 文章目录 1、CDC简介1.1 什么是CDC1.2 CDC的种类1.3 Flink-CDC 2、FlinkCDC案例实操2.1 开启MySQL Binlog并重启MySQL2.2 FlinkSQL方式的应用2.2.1 导入依赖2.2.2…

解密zkLogin:探索前沿的Sui身份验证解决方案

由于钱包复杂性导致的新用户入门障碍是区块链中一个长期存在的问题&#xff0c;而zkLogin是其简单的解决方案。通过使用前沿的密码学和技术&#xff0c;zkLogin既优雅又复杂。本文深入探讨了zkLogin的工作原理&#xff0c;涵盖了用户和开发者的安全性方面&#xff0c;并解释了S…