【Linux】线程池读写锁

news2025/1/11 2:16:14

文章目录

  • 线程池
    • 应用场景
    • 线程池原理
    • 构造线程池
    • 代码实现
  • 读写锁
    • 应用场景
    • 读写锁的三种状态
    • 读写锁的接口
      • 初始化接口
      • 销毁接口
      • 以读模式加锁
      • 以写模式加锁
      • 解锁接口
    • 常见问题
  • 乐观锁/悲观锁
    • 乐观锁
    • 悲观锁
    • 自旋锁

线程池

应用场景

线程池不仅要提高程序运行效率,还要提高程序处理业务的种类,提高程序运行效率自然要创建多个线程

说到提高业务的种类,应该不难想到switch case语句结合if语句来实现,示例如下:

在这里插入图片描述

这种实现不同业务的方式比较简单,但是相对也很有局限性,如果业务种类比较多,这种分支语句就不适用了。

一个线程被创建之后,只能执行一个线程入口函数,后续是没有办法更改的,基于这种场景,线程可能执行的代码也就是固定了。换句话说即使线程入口函数当中有很多分支,但是相对来说线程执行的路线都是固定的,要么时A分支,要么时B分支,要么是C分支。这里的分支是指类似if,else语句。这里如果我们要是在后续再想新增加新的业务判断逻辑,那就只能在原有线程入口函数进行增加写代码,这样就会导致一个线程入口函数的代码愈来愈多。那么如果代码的耦合性过高,只要一个地方出现错误,我们查找bug时就会十分头疼。
所以为了能让线程执行不同的业务代码,就要考虑线程从队列中获取元素的身上下功夫。让线程可以通过线程元素来执行不同的代码。

线程池原理

线程池原理 = 一堆线程 + 线程安全的队列

在这里插入图片描述

构造线程池

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

代码实现

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<queue>
using namespace std;

//定义队列元素的类型
//   数据
//   处理数据的方法

typedef void(*Handler)(int data);
//创建一个新类型,Handler是一个指向没有返回值,接收一个整形参数的函数指针

class QueueData{
public:
  QueueData(){

  }
  QueueData(int data, Handler handler){
    data_ = data;
    handler_ = handler;
  }

  //如何通过函数处理数据
  void Run(){
    handler_(data_);
  }
private:
  int data_;//要处理的数据
  Handler handler_;
  //处理数据的函数(要保存一个函数的地址)
  //Handler 函数指针
  //handler 函数指针变量,保存函数地址
};

//线程安全队列
//互斥 + 同步
//条件变量
class SafeQueue{
public:
  SafeQueue(){
    capacity_ = 1;
    pthread_mutex_init(&lock_, NULL);
    pthread_cond_init(&prod_cond_, NULL);
    pthread_cond_init(&cons_cond_, NULL);
  }
  ~SafeQueue(){
    pthread_mutex_destroy(&lock_);
    pthread_cond_destroy(&prod_cond_);
    pthread_cond_destroy(&cons_cond_);
  }
  //入队函数
  void Push(QueueData& data){
    pthread_mutex_lock(&lock_);
    while(que_.size() >= capacity_){
      pthread_cond_wait(&prod_cond_, &lock_);
    }
    que_.push(data);
    pthread_mutex_unlock(&lock_);
    pthread_cond_signal(&cons_cond_);
  }

  //出队函数
  void Pop(QueueData* data, int flag_exit){
    pthread_mutex_lock(&lock_);
    while(que_.empty()){
      if(flag_exit == 1){
        pthread_mutex_unlock(&lock_);
        pthread_exit(NULL);
      }
      pthread_cond_wait(&cons_cond_, &lock_);
      //条件变量等待函数,将调用该函数的线程放到PCB等待队列中
      //lock_:该线程等待的互斥锁
    }
    *data = que_.front();
    que_.pop();
    pthread_mutex_unlock(&lock_);
    pthread_cond_signal(&prod_cond_);
  }

  void BroadcaseAllConsume(){
    pthread_cond_broadcast(&cons_cond_);
  }
private:
  queue<QueueData> que_;
  size_t capacity_;
  //互斥锁
  pthread_mutex_t lock_;

  //同步
  pthread_cond_t prod_cond_;
  //生产者的条件变量
  pthread_cond_t cons_cond_;
  //消费者的条件变量
  //线程池中的一堆线程,在逻辑上可以认为是消费者
};

//线程池:一堆线程 + 线程安全的队列
class ThreadPool{
public:
  ThreadPool(){
  }
  ~ThreadPool(){
    if(sq_ != NULL){
      delete sq_;
    }
  }
  int InitThreadPool(int thread_count){
    //0:线程继续运行
    //1:线程退出
    flag_exit_ = 0;
    sq_ = new SafeQueue;
    if(sq_ == NULL){
      printf("Init thread pool failed\n");
      return -1;
    }
    thread_count_ = thread_count;
    
    for(int i=0; i<thread_count_; ++i){
      pthread_t tid;
      int ret = pthread_create(&tid, NULL, worker_start, (void*)this);
      if(ret < 0){
        thread_count--;
        continue;
      }
    }
    //根据目前线程的数量来判断创建线程是否成功
    if(thread_count_ <= 0){
      printf("create thread all failed\n");
      return -1;
    } 
    return 0;//初始化成功
  }

  //线程池的使用接口
  //只需要给使用者提供Push接口,让使用者可以将线程push到队列中就好
  //而pop接口不需要提供,因为线程池中的线程可以自己调用到
  void Push(QueueData& qd){
    sq_->Push(qd);
  }

  //线程入口函数
  static void* worker_start(void* arg){
    pthread_detach(pthread_self());
    //从队列当中拿元素
    //处理元素
    ThreadPool* tp = (ThreadPool*)arg;
    while(1){
      QueueData qd;
      tp->sq_->Pop(&qd, tp->flag_exit_);
      qd.Run();
    }
  }
  void thread_pool_exit(){
    flag_exit_ = 1;
    sq_->BroadcaseAllConsume();
  }
private:
  //线程安全的队列
  SafeQueue* sq_;
  //线程池中线程的数量
  int thread_count_;
  //标志线程是否退出的标志位
  int flag_exit_;
};

void Deal1(int data){
  printf("I am Deal1, i deal %d\n", data);
}

void Deal2(int data){
  printf("hello! I am Deal2, i deal %d\n", data);
}

int main(){
  //创建线程池
  //往线程池中放数据
  ThreadPool tp;
  int ret = tp.InitThreadPool(2);
  if(ret < 0) return 0;
  for(int i=0; i<100; ++i){
    QueueData qd(i, Deal2);
    tp.Push(qd);
  }
  tp.thread_pool_exit();
  while(1){
    sleep(1);
  }
  return 0;
}

读写锁

应用场景

  • 大量读取,少量写的场景
  • 允许多个线程并行读,多个线程互斥写

读写锁的三种状态

  • 以读模式加锁的状态
  • 以写模式加锁的状态
  • 不加锁的状态

读写锁的接口

初始化接口

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr)

参数:

  • pthread_rwlock_t:读写锁的类型,rwlock:传递读写锁
  • attr:NULL,默认属性

销毁接口

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

以读模式加锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//阻塞接口

允许多个线程以并行已读模式获取读写锁

引用计数:用来记录当前读写锁有多少个线程以读模式获取了读写锁

1.每当有线程以读模式进行加锁,引用计数++;
2.每当读模式的线程释放锁,引用计数–;

引用计数的作用,当引用计数为0时,那么证明当前没有线程在进行读取操作,那么写的线程就可以获取到这把读写锁进行写。

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//非阻塞接口

以写模式加锁

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);//非阻塞接口
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//阻塞接口

解锁接口

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//解锁接口

常见问题

现在如果有线程A和线程B在对一个资源进行读取,此时来了一个线程C要以写的方式获取这把读写锁,然后又来了一个线程D要进行读取,这里问线程C要不要等待线程D也读取完之后再对资源进行写?

这里读写锁的内部是有个机制的:如果读写锁已经在读模式打开了,有一个线程A想要以写模式打开获取读写锁,则需要等待,如果在等待期间,又来了读模式加锁的线程,那读模式的线程要等待写线程先执行完再说,因为如果这时读取的线程可以获取这把锁,读写锁本来就是大量读少量写的使用场景,那么就会导致写的线程一直拿不到这把锁,这是不合理的。线程就会饥饿。

乐观锁/悲观锁

乐观锁

针对某个线程访问临界区修改数据的时候,乐观锁认为只有该线程在修改,大概率不会存在并行的情况,所以修改数据不加锁,但是,在修改完毕,进行更新的时候,进行判断,例如:版本号控制、CAS无锁编程

悲观锁

针对某个线程访问临界区修改数据的时候,都会认为可能有其他线程并行修改的情况发生,所以在线程修改数据之前就进行加锁,让多个线程互斥访问。悲观锁有:互斥锁、读写锁、自旋锁等

自旋锁

自旋锁 (busy-waiting类型) 和互斥锁 (sleep-waiting类型)的区别:

1.自旋锁加锁时,加不到锁,线程不会切换 (时间片没有到的情况,时间片到了,也会线程切换),会持续的尝试拿锁, 直到拿到自旋锁
2.互斥锁加锁时, 加不到锁,线程会切换(时间片没有到,也会切换),进入睡眠状态,当其他线程释放互斥锁(解锁)之后, 被唤醒。在切换回来,进行抢锁

3.自旋锁的优点:因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。
4.自旋锁的缺点:自旋锁一直占用着CPU,他在未获得锁的情况下,一直运行(自旋),所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低
5.适用于临界区代码较短时(直白的说: 临界区代码执行时间短)的情况, 使用自旋锁效率比较高。因为线程不用来回切换
6.当临界区当中执行时间较长, 自旋锁就不适用了, 因为拿不到锁会占用CPU一直抢占锁。

自旋锁API:

pthread_spin_init:初始化自旋锁

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

pthread_spin_destroy:销毁自旋锁

int pthread_spin_destroy(pthread_spinlock_t *lock);

pthread_spin_lock:尝试获取自旋锁,如果自旋锁已经被锁定,线程将自旋等待。

int pthread_spin_lock(pthread_spinlock_t *lock);

pthread_spin_trylock:尝试获取自旋锁,如果自旋锁已经被锁定,它不会等待,而是立即返回

int pthread_spin_trylock(pthread_spinlock_t *lock);

pthread_spin_unlock:释放自旋锁,允许其他线程获取它

int pthread_spin_unlock(pthread_spinlock_t *lock);

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

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

相关文章

读SQL学习指南(第3版)笔记13_读后总结与感想兼导读

1. 基本信息 SQL学习指南(第3版) Learning SQL, Third Edition [美] 艾伦博利厄 &#xff08;Alan Beaulieu&#xff09; 人民邮电出版社,2022年4月出版 1.1. 读薄率 书籍总字数424千字&#xff0c;笔记总字数25969字。 读薄率25969424000≈6.13% 1.2. 读厚方向 SQL入门经…

探索多线程编程:线程的本质、状态和属性

目录 什么是线程线程状态新建线程可运行线程阻塞和等待线程终止线程 线程属性优先级线程名守护线程中断线程未捕获异常的处理器 在现代计算机编程中&#xff0c;多线程是一个重要而强大的概念。它使得我们能够更有效地利用多核处理器、提高程序性能并实现并发操作。 什么是线程…

docker从零部署jenkins保姆级教程(下)

上一篇文章&#xff0c;我们完成了以下工作。 1)、docker部署jenkins 2)、建立第一个jenkins job 3)、通过jenkins job自动编译构建我们的github项目 上面所做的3个工作&#xff0c;其实都是为了这一篇文章打基础&#xff0c;不管是部署docker还是部署jenkins&#xff0c;我们最…

d435i 相机和imu标定

一、IMU 标定 使用 imu_utils 功能包标定 IMU&#xff0c;由于imu_utils功能包的编译依赖于code_utils&#xff0c;需要先编译code_utils&#xff0c;主要参考 相机与IMU联合标定_熊猫飞天的博客-CSDN博客 Ubuntu20.04编译并运行imu_utils&#xff0c;并且标定IMU_学无止境的…

补码:将减法运算转化为另一种形式的加法运算

文章目录 解析 个人见解&#xff0c;如有错误&#xff0c;请多包涵。 解析 对于人来说&#xff0c;减法是简单容易的。 被减数和减数列式相减&#xff0c;从低位到高位分别计算&#xff0c;有需要的借位就可以了。 这是一种可以在计算机上成立的理论方案&#xff0c;但是由于…

SpringMVC常用注解介绍及参数传递说明

前言 上一篇文章介绍了SpringMVC是什么以及它的工作流程和核心组件&#xff0c;介绍入门示例时&#xff0c;提到了RequestMapping注解&#xff0c;那么这篇文章就来介绍SpringMVC中更多的常用的注解&#xff0c;以及它的参数传递。 一. SpringMVC常用注解 1.1 RequestParam …

Homebrew安装cocoapods: zsh: command not found: brew解决方法

问题描述: 通过Homebrew安装cocoapods时,输入命令行 brew install cocoapods出现如下报错: zsh: command not found: brew zsh:找不到命令&#xff1a;brew 问题解决: 使用以下命令,重新安装Homebrew. /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/ra…

永恒之黑_CVE-2020-0796漏洞复现

永恒之黑&#xff1a;CVE-2020-0796漏洞复现 目录 永恒之黑&#xff1a;CVE-2020-0796漏洞复现漏洞介绍漏洞影响范围漏洞复现1.环境准备2.复现过程 漏洞介绍 本漏洞源于SMBv3没有正确处理压缩的数据包&#xff0c;在解压数据包的时候使用客户端传过来的长度进行解压时&#xf…

机器学习——boosting之提升树(未完)

提升树和adaboost基本流程是相似的 我看到提升树的时候&#xff0c;懵了 这…跟adaboost有啥区别&#xff1f;&#xff1f;&#xff1f; 直到看到有个up主说了&#xff0c;我才稍微懂 相当于&#xff0c;我在adaboost里的弱分类器&#xff0c;换成CART决策树就好了呗&#xff1…

springboot + activiti实现activiti微服务化

概述 本文介绍如何将springbootactiviti进行整合,并配合eureka,zuul和feign实现activiti的微服务化,将流程控制和业务逻辑分离. 并实现了几个比较特殊的功能,比如时间段委托(某人请假或出差,出差时间内,所有待办交给被委托人处理),比如节点的无限级加签功能(流程本身有不确定性…

php 获取今日、昨日、上周、本月的起始时间戳和结束时间戳的方法非常简单

php 获取今日、昨日、上周、本月的起始时间戳和结束时间戳的方法&#xff0c;主要使用到了 php 的时间函数 mktime。下面首先还是以示例说明如何使用 mktime 获取今日、昨日、上周、本月的起始时间戳和结束时间戳&#xff0c;然后在介绍一下 mktime 函数作用和用法。非常简单哦…

vue3组件通信学习笔记

1、Prop 父组件 <template><div class"parent"><h1>我是父元素</h1><Child :msg"msg"></Child></div> </template><script setup> import Child from ./Child.vue let msg ref(我是父组件的数据…

单片机采集传感器数据(整形,浮点型)modbus上传

浮点型数据 占两个寄存器&#xff08;四个字节&#xff09; short 整形 占一个寄存器 &#xff08;两个字节&#xff09; 注意&#xff01;&#xff01;&#xff01;&#xff01; stm32 是小端模式&#xff0c;而modbus解析数据是大端模式 所以先发送高字节 如int a16777220…

【QML】使用 QtQuick2的ListView创建一个列表(一)

qtquick2版本和qtquick1版本分别提供了一个ListView组件供使用&#xff0c;两个组件在使用上和属性的提供上还是有比较大的差异的&#xff0c;因为qtquick2是新的&#xff0c;所以就以改版本为基础学习如何使用&#xff1b; 首先&#xff0c;要知道ListView提供了那些属性提供修…

2023年智能家居占消费电子出货量28%,蓝牙Mesh照明占据重要位置

市场研究机构 TechInsights 的最新报告显示&#xff0c;预计 2023 年全球消费者在智能家居相关硬件、服务和安装费方面的支出将复苏&#xff0c;达到 1310 亿美元&#xff0c;比 2022 年增长 10%。TechInsights 表示&#xff0c;消费者在智能家居系统和服务上的支出将继续强劲增…

【UIPickerView案例05-省市选择界面数据展示 Objective-C语言】

一、省市选择界面数据展示 1.省市选择界面数据展示,就是这样的一个东西 我们接下来,看我们第二个案例,就是这个省市选择, 左边选择一个省,右边就把这个省所有的市展示出来 比如,我现在展示的是山东的城市, 我选择一个山西 第一步干嘛,是不是也是分析它的界面 1)上…

Android Automotive编译

系统准备 安装系统 准备一台安装Ubuntu系统的机器&#xff08;windows系统的机器可以通过WSL安装ubuntu系统&#xff09; 安装docker 本文使用docker进行编译&#xff0c;因此提前安装docker。参考网络链接安装docker并设置为不使用sudo进行docker操作。 参考链接&#xff…

B-Tree 索引和 Hash 索引的对比

分析&回答 B-Tree 索引的特点 B-tree 索引可以用于使用 , >, >, <, < 或者 BETWEEN 运算符的列比较。如果 LIKE 的参数是一个没有以通配符起始的常量字符串的话也可以使用这种索引。 有时&#xff0c;即使有索引可以使用&#xff0c;MySQL 也不使用任何索引。…

2023 最新 Git 分布式版本控制系统介绍和下载安装使用教程

Git 基本概述 Git 是一个开源的分布式版本控制系统&#xff0c;用于敏捷高效地处理任何或大或小的项目。 集中式和分布式的区别&#xff1f; 最常见的集中式版本控制系统是SVN&#xff0c;版本库是集中放在中央处理器中的&#xff0c;而干活的时候&#xff0c;用的都是自己电…

类和对象(Java)

目录 一、面向对象的初步认知1、什么是面向对象2、面向对象与面向过程 二、类和类的实例化1、什么是类2、类的实例化3、类和对象的说明 三、this引用1、为什么要有this引用2、什么是this引用3、this引用的特性 四、对象的构造及初始化1、如何初始化对象2、构造方法 五、封装1、…