『Linux』第九讲:Linux多线程详解(六 - 完结)_ 线程池 | 读写锁

news2024/12/22 23:12:17

「前言」文章是关于Linux多线程方面的知识,上一篇是 Linux多线程详解(五),今天这篇是 Linux多线程详解(六),也是多线程最后一篇,内容大致是线程池,讲解下面开始!

「归属专栏」Linux系统编程

「笔者」枫叶先生(fy)

「座右铭」前行路上修真我

「枫叶先生有点文青病」

「每篇一句」

君子坦荡荡,小人长戚戚。
——《论语·述而》

目录

一、线程池

1.1 概念

1.2 实现

二、线程安全的单例模式

三、STL、智能指针和线程安全

四、其他常见的各种锁

五、读写锁(读者写者模型)


一、线程池

1.1 概念

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

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

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

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

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

线程池的应用场景:

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

线程池使用:

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

1.2 实现

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

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

实现代码如下:

 ThreadPool.hpp

#pragma once

#include "Thread.hpp"
#include "LockGuard.hpp"
#include <iostream>
#include <pthread.h>
#include <vector>
#include <queue>
#include <unistd.h>

static const int gnum = 5;//一次创建多少个线程
//声明,ThreadData类用到ThreadPool类
template <class T>
class ThreadPool;
//线程的上下文,当一个结构体使用
template <class T>
class ThreadData
{
public:
    ThreadPool<T> *threadpool;//线程
    std::string name;//线程名称

public:
    ThreadData(ThreadPool<T> *tp, const std::string &n) : threadpool(tp), name(n){}
};

template <class T>
class ThreadPool
{
public:
    ThreadPool(const int &num = gnum) : _num(num)
    {
        pthread_mutex_init(&_mutex, nullptr); //初始化互斥锁
        pthread_cond_init(&_cond, nullptr);
        for (int i = 0; i < _num; i++)
        {
            _threads.push_back(new ThreadNs::Thread()); //创建线程并push_back到_threads里面,便于管理线程
        }
    }
    //开始运行创建的所有线程,阻塞等待任务的到来
    void run()
    {
        for (const auto &t : _threads)
        {
            ThreadData<T> *td = new ThreadData<T>(this, t->threadname());
            t->start(handlerTask, td);
            std::cout << t->threadname() << ", start..." << std::endl;
        }
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for (const auto &t : _threads)
            delete t;
    }
    // 对任务队列里增加一个任务
    void push(const T &in)
    {
        LockGuard lockguard(&_mutex);
        _task_queue.push(in);
        pthread_cond_signal(&_cond);
    }
     // 拿走任务队列里的一个任务
    T pop()
    {
        T task = _task_queue.front();
        _task_queue.pop();
        return task;
    }

public:
    // 共静态方法进行调用
    void mutexLockQueue() { pthread_mutex_lock(&_mutex); }
    void mutexUnlockQueue() { pthread_mutex_unlock(&_mutex); }
    bool isQueueEmpty() { return _task_queue.empty(); }
    void threadCondWait() { pthread_cond_wait(&_cond, &_mutex); }
    pthread_mutex_t *mutex() { return &_mutex; }

private:
    // 类静态成员方法无法直接调用类的成员方法和成员变量
    static void *handlerTask(void *args)
    {
        ThreadData<T> *td = static_cast<ThreadData<T> *>(args);
        LockGuard lockguard(td->threadpool->mutex());//LockGuard自己封装的RAII风格的锁
        T task;
        {
            while (td->threadpool->isQueueEmpty())
            {
               td->threadpool->threadCondWait();
            }
            task = td->threadpool->pop();
        }
        std::cout << td->name << " 获取了一个任务: " << task.toTaskString() << " 并处理完成,结果是:" << task() << std::endl;
        delete td;
        return nullptr;
    }

private:
    int _num; //创建线程的数量
    std::vector<ThreadNs::Thread *> _threads; //用于管理线程
    std::queue<T> _task_queue; //任务队列
    pthread_mutex_t _mutex; //互斥锁
    pthread_cond_t _cond; //条件变量
};

 由于代码过多,就不一一贴完了,代码链接:code_linux/code_202304_26/9_ThreadPool · Maple_fylqh/code - 码云 - 开源中国 (gitee.com)

主函数Main.cc

#include "ThreadPool.hpp"
#include "Task.hpp"
#include <memory>
#include <unistd.h>

int main()
{
    std::unique_ptr<ThreadPool<Task>> tp(new ThreadPool<Task>());
    tp->run();

    int x, y;
    char op;
    while (1)
    {
        std::cout << "请输入数据1# ";
        std::cin >> x;
        std::cout << "请输入数据2# ";
        std::cin >> y;
        std::cout << "请输入你要进行的运算#";
        std::cin >> op;
        Task task(x, y, op, mymath);
        tp->push(task);
        // std::cout << "你刚刚录入了一个任务: " << task.toTaskString() << ", 确认提交吗?[y/n]# ";
        // char confirm;
        // std::cin >> confirm;
        // if (confirm == 'y')
        //     tp->push(task);
       
        sleep(1);
    }
    return 0;
}

 测试运行:

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

二、线程安全的单例模式

这里在C++篇章已经谈过,这里不再赘述,链接(26条消息) 『C++』特殊类设计_枫叶先生的博客-CSDN博客

注意事项:

  1. 加锁解锁的位置
  2. 双重 if 判定, 避免不必要的锁竞争
  3. volatile关键字防止过度优化

三、STL、智能指针和线程安全

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

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

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

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

四、其他常见的各种锁

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

其他锁不是很重要,就不解释了,解释一下自旋锁:

  • 之前学的互斥锁,信号量这些,一旦申请失败,线程就会被阻塞挂起,我们称这样的锁为挂起等待锁,一直到锁被释放
  • 而自旋锁如果申请失败,线程并不会挂起等待,它会选择自旋(轮询的过程),循环检查锁的状态是否被释放
  • 自旋锁的实现通常使用原子操作来保证线程安全。当一个线程成功获取自旋锁时,它会将锁的状态设置为“已占用”,其他线程尝试获取锁时会发现锁已经被占用,于是它们会不断地自旋等待,直到锁被释放
  • 自旋锁的优点是在锁竞争不激烈的情况下,可以避免线程的上下文切换,从而提高锁的性能。但是在锁竞争激烈的情况下,自旋锁会导致大量的线程在自旋等待,从而浪费CPU资源,降低系统的性能。因此,自旋锁通常适用于锁竞争不激烈的情况下

五、读写锁(读者写者模型)

除生产消费模型之外,还有非常经典的一个模型,就是读者写者模型

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

读写锁概念

读写锁是一种特殊的锁,用于解决读者写者问题,它允许多个读者同时访问共享资源,但只允许一个写者进行访问

读者写者模型概念

  • 读者写者模型是并发编程中一种用于解决多个进程或线程访问共享资源问题的同步机制。
  • 读者和写者代表着访问共享资源的不同角色。
  • 写者就是生产者,负责生产数据;读者就是消费者,用于消费数据
  • 读者写者也是通过共享缓冲区进行通信,以此实现数据的传输和处理
  • 在读者写者模型中,写者是负责生产数据的,读者是用于消费数据,注意,这里的消费是是指只读取数据,并不拿走数据

这就类似于我们以前在学校里面画的黑板报:

  • 画黑板报的同学是写者
  • 黑板报是共享的缓冲区,称为交易场所
  • 观看黑板报的同学称为读者

注意:这里限制一下,画黑板报的同学在同一时刻,只能有一名同学在画黑板报,黑板报也只有一块

对读者写者模型的角色之间的关系分析

  • 一个读者或多个读者对应:一个线程或多个线程
  • 一个写者或多个写者对应:也是一个线程或多个线程
  • 黑板报对应:共享资源

角色之间的关系分析:

  • 写者与写者之间:互斥关系(竞争画黑板报)
  • 读者与读者之间:没有关系(你读你的,我读我的(共同读同一块黑板报,不会说一个个排队看黑板报)) 
  • 写者与读者之间:互斥 && 同步(互斥:写者在画黑板报,黑板报还未画完(未写完数据),此时不允许读者来看黑板报(此时数据不完全),如果进行读取,只会读到残缺的数据。当读者在读黑板报,不允许写者进行画黑板报(写数据),要不然读者数据都没读完就被你写者新写的数据给覆盖掉了。同步:写者写完数据,就要等待读者读取完数据;读者读完数据了,就要等待写者重新进行写入新的数据)

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

读者写者模型的特点

321原则(便于记忆)

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

读写锁的接口

定义一个读写锁
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);//解锁共用

 设置读写优先

设置读写优先
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/*
pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况

PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和PTHREAD_RWLOCK_PREFER_READER_NP 一致

PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
*/

读写锁的行为: 

Linux系统编程暂时完结,告一段落 

--------------------- END ----------------------

「 作者 」 枫叶先生
「 更新 」 2023.6.5
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
          或有谬误或不准确之处,敬请读者批评指正。

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

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

相关文章

什么?英语不好?这所211可以不考英语!

本期为大家整理热门院校“哈尔滨工程大学810”的择校分析&#xff0c;这个择校分析专题会为大家结合&#xff1a;初试复试占比、复试录取规则&#xff08;是否公平&#xff09;、往年录取录取名单、招生人数、分数线、专业课难度等进行分析。希望能够帮到大家! –所有数据来源…

KDJJC-80绝缘油介电强度测试仪

一、概述 测试仪&#xff08;单杯&#xff09;是我公司科研技术人员&#xff0c;依据国家标准GB507-1986及行标DL/T846.7-2004的有关规定&#xff0c;发挥自身优势&#xff0c;经过多次现场试验和长期不懈努力&#xff0c;精心研制开发的高准确度、数字化工业仪器。 为满足不同…

初步了解SpringCloud微服务架构

✅作者简介&#xff1a;大家好&#xff0c;我是Cisyam&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Cisyam-Shark的博客 &#x1f49e;当前专栏&#xff1a; 微服务探索之旅 ✨特色专…

Apikit SaaS 10.9.0 版本更新:接口测试支持通过 URL 请求大型文件,覆盖更多场景的文件请求测试

Hi&#xff0c;大家好&#xff01; Eolink Apikit 即将在 2023年 6月 8日晚 18:00 开始更新 10.9.0 版本。本次版本更新主要是对多个应用级资源合并&#xff0c;并基于此简化付费套餐和降低费率。 本次应用合并是为了接下来更好的发挥 Eolink Apikit 的优势&#xff0c;提供 …

Web前端-React学习

React基础 React 概述 React 是一个用于构建用户界面的JavaScript库。 用户界面&#xff1a; HTML页面&#xff08;前端&#xff09; React主要用来写HTML页面&#xff0c; 或构建Web应用 如果从MVC的角度来看&#xff0c;React仅仅是视图层&#xff08;V&#xff09;,也就…

多目标建模loss为什么最好同时收敛?

多目标的多个loss是否同时收敛最好&#xff1f; 假设 task A的独有参数 W a W_a Wa​task B的独有参数 W b W_b Wb​task A和 task B的共享的参数 W s W_s Ws​ 那么 l o s s l o s s a l o s s b loss loss_a loss_b losslossa​lossb​ 假设损失函数为 f f f&…

【DepthFilter】深度滤波器

14讲P326-327 函数实现一个深度滤波器&#xff0c;用于计算图像中某个像素点的深度值。算法步骤的含义和含义&#xff1a; 将当前帧的像素点和参考帧的像素点通过三角化计算深度。将参考帧到当前帧的变换矩阵 T_C_R 转换为当前帧到参考帧的变换矩阵 T_R_C。将参考帧像素点 pt_…

Docker超详细基础使用(带图)

目录 安装ubuntu 基本使用命令 docker run 容器名 延伸命令 启动ubuntu 查看所有正在运行的容器 指定容器别名启动 doker ps 延伸命令 退出容器 重新进入正在运行的容器 启动容器 删除已停止的容器 强制删除容器 查看容器日志 查看容器内部运行的进程 ​编辑 查看容…

Axure教程—分段滑动条

本文将教大家如何用AXURE中动态面板制作单分段滑动条 一、效果 预览地址&#xff1a;https://c00qrq.axshare.com 下载地址&#xff1a;https://download.csdn.net/download/weixin_43516258/87881401?spm1001.2014.3001.5503 二、功能 滑块滑动到相应的浮点&#xff0c;显示…

【SVN】SVN查看日志时报错:联系服务器时出现问题,条目不可读

目录 0.背景介绍 1.问题原因 2.解决步骤 0.背景介绍 环境&#xff1a;SVN服务器在ubuntu下&#xff0c;SVN客户端在windows下。 最近在搭ubuntu下的SVN的服务器&#xff0c;然后再windows下用SVN客户端将文件上传至服务器保管。 windows下想查看日志时&#xff0c;报错【…

React学习7 redux

redux的三个核心概念 1. action 动作的对象包含2个属性 type&#xff1a;标识属性, 值为字符串, 唯一, 必要属性data&#xff1a;数据属性, 值类型任意, 可选属性例子&#xff1a;{ type: ADD_STUDENT,data:{name: tom,age:18} } 2. reducer 用于初始化状态、加工状态。加工…

健身器材开发方案,带有12位ADC检测、LED屏显的语音IC-N9300

身体锻炼过程中所使用到的所有物品&#xff0c;健身器材类体育用品则主要涉及健身领域&#xff0c;包括室外健身器材和室内健身器材。 每天清晨或傍晚跑跑步&#xff0c;不仅能提高身体素质同时能得到很好的瘦身效果。然而大部分人觉得慢跑等运动过于无聊没有给予运动者本身进行…

【Redis编译安装】---redis-4.0.8

【Redis编译安装】---redis-4.0.8 &#x1f53b; 一、Redis 编译安装1.1 ⛳ 上传解压1.2 ⛳ 升级gcc环境1.3 ⛳ 编译安装1.3.1 &#x1f341;cd 到redis解压目录1.3.2 &#x1f341;编译1.3.3 &#x1f341; make test1.3.4 &#x1f341; 安装tcl-8.51.3.5 &#x1f341; 安装…

shell 第十一章

1.写一个库函数&#xff0c;用定时任务调用这个库函数&#xff0c;每月1号执行 1.sh: 1.1.sh: 2.以免交互的方式实现 ssh 远程登录&#xff0c;密码错误也直接退出&#xff0c;不用人干预 3.以免交互的方式&#xff0c;实现磁盘分区、格式化、挂载

Keysight 34970A数据采集记录仪产品介绍

Keysight 34970A数据采集记录仪 Keysight 34970A数据采集记录仪开关单元由一个 3 插槽主机和一个内置的 6 1/2 位数字万用表组成。每个通道可以单独配置&#xff0c;以测量 11 种不同功能之一&#xff0c;这样既不会增加成本&#xff0c;也不必使用复杂的信号调理附件。您可用…

【干货】PCB材料选择与性能比较

PCB板被广泛应用于电子行业&#xff0c;作为电子设备的重要组成部分之一&#xff0c;负责连接各种电子元件。PCB板的性能直接影响着电子设备的质量和稳定性。而PCB板的材料选择则是影响PCB板性能的关键因素之一。本文将对常见PCB材料进行比较分析&#xff0c;以便于选择适合的材…

直线模组的应用案例

直线模组最早是在德国开发使用的&#xff0c;因其具有单体运动速度快、重复定位精度高、本体质量轻、占设备空间小、寿命长等特点&#xff0c;被广泛应用在各种各样的机械设备中&#xff0c;尤其是自动化领域&#xff0c;基本上都能看到直线模组的身影&#xff0c;那么&#xf…

Target DVS EDI项目开源介绍

近期为了帮助广大用户更好地使用 EDI 系统&#xff0c;我们根据以往的项目实施经验&#xff0c;将成熟的 EDI 项目进行开源。用户安装好知行之桥EDI系统之后&#xff0c;只需要下载我们整理好的示例代码&#xff0c;并放置在知行之桥指定的工作区中&#xff0c;即可开始使用。 …

小程序项目结构与组件基础

文章和代码已经归档至【Github仓库&#xff1a;https://github.com/timerring/front-end-tutorial 】或者公众号【AIShareLab】回复 小程序 也可获取。 文章目录 项目结构了解项目的基本组成结构小程序页面的组成部分json配置文件的作用全局配置文件app.jsonproject.config.jso…

数据仓库基础知识

数据仓库 企业信息应用现状企业对应用集成的需求1. 什么是BI1.1 BI的定义1.2 BI要做的事情1.3 BI的智能1.4 BI应用架构1.5 BI系统架构1.6 BI应用带来的关键效益 2. 什么是数据仓库2.1 数据仓库的概念2.2 数据仓库的特性 3. 数据仓库设计中的几个重要概念3.1 ETL3.2 数据集市&am…