Linux系统编程——多线程[补充]:懒汉模式自旋锁读者写者问题

news2024/11/25 7:47:26

0.关注博主有更多知识

操作系统入门知识合集

目录

1.单例设计模式

1.1将线程池设计为懒汉方式实现的单例模式

1.2线程安全版本的懒汉方式

2.STL、智能指针与线程安全

3.自旋锁

4.读者写者问题

1.单例设计模式

在一些软件设计场景当中,要求某些类只能具有一个对象,这样的模式我们就称为单例。例如服务器类,程序加载运行后,我们希望服务器类只实例化出一个服务器对象。

单例模式分为饿汉实现和懒汉实现。以一个通俗的例子来说明饿汉和懒汉:

  1.饿汉说的就是吃完饭,立刻洗碗,这样做就可以在下一次吃饭的时候不用洗碗

  2.懒汉指的是吃完饭,先不洗碗,直到下一次吃饭时再洗碗

那么放在程序设计的角度,就可这么理解饿汉和懒汉:

  1.饿汉指的是程序还没有开始运行就实例化一个对象。这种对象通常是静态变量或者全局变量,因为在C++程序中,main()函数是程序的入口,在执行main()函数之前,全局变量已经被定义好了。

  2.懒汉指的是程序需要使用对象时才实例化对象

事实上,懒汉实现的核心思想就是"延迟加载",能够优化程序的启动速度。为什么懒汉能够优化程序的启动速度?因为饿汉的对象在程序运行之前就要创建,如果该对象的创建需要使用大量的资源和时间,那么就会延迟程序进入main()函数;那么懒汉的对象是在我们需要使用对象时才实例化出对象,这就意味着程序相较于饿汉模式能够提前一些时间进入main()函数。

事实上我们调用的new或malloc()也是一种延迟加载的思想,当我们new或malloc()成功申请一块空间时,我们就真的拥有这块空间了吗?当然未必,操作系统可能只会将地址空间的地址范围稍微扩大一些,让我们的进程看到的虚拟内存又扩大了一些,从而让我们感觉到空间已经申请到了。实际上真实的物理内存空间并未属于我们,只有我们真正向申请的空间进行操作的时候,操作系统才会执行缺页中断、重新建立页表映射等等操作,最后将物理内存空间映射到我们的地址空间当中。我们试着逆向思考一下,如果我们使用new或malloc()申请了一块空间之后立马获得该空间,但是我们不去使用它,这不就是一种"占着茅坑不拉屎"的行为吗?这不就是一种浪费系统的资源的做法吗?

所以我们主要讨论单例模式中的懒汉实现

1.1将线程池设计为懒汉方式实现的单例模式

以上一篇博客实现的线程池为例,假设在某个应用程序当中需要使用线程池,并且希望它是单例的,那么我们可以这么实现线程池:

    template <class T>
    class threadPool;

    template <class T>
    class threadData /*传递给线程例程的参数*/
    {
    public:
        threadPool<T> *_this;
        string _name;
    };

    template <class T>
    class threadPool
    {
    private:
#define INIT_THREAD_NUM 10
        static void *start_routine(void *args);
    private:
        void queueLock() { pthread_mutex_lock(&_mutex); }
        void queueUnlock() { pthread_mutex_unlock(&_mutex); }
        bool isQueueEmpty() { return _taskQueue.empty(); }
        void queueWait() { pthread_cond_wait(&_cond, &_mutex); }
        T taskPop();
        pthread_mutex_t *mutex() { return &_mutex; }
    private:
        /*将构造函数作为私有,并且禁用拷贝构造和赋值重载
         *目的就是为了不允许在类外定义对象*/
        threadPool(int num = INIT_THREAD_NUM);
        threadPool(const threadPool &tp) = delete;
        threadPool &operator=(const threadPool &tp) = delete;
    public:
        ~threadPool();
        void start();
        void push(const T &in);

        /*给定一个静态方法,用来获取单例对象的指针
         *静态方法可以不用对象访问*/
        static threadPool<T> *getInstance()
        {
            /*如果单例指针为空,说明对象还没有被实例化过*/
            if(_singleton == nullptr)
            {
                _singleton = new threadPool<T>();
            }
            return _singleton;
        }

    private:
        vector<Thread *> _threadVec;
        queue<T> _taskQueue;
        pthread_mutex_t _mutex;
        pthread_cond_t _cond;

        /*单例指针*/
        static threadPool<T> *_singleton;
    };
    template <class T>
    threadPool<T> *threadPool<T>::_singleton = nullptr;
// 测试用例

#include "singletonPthreadPool.hpp"
using namespace threadpool;

#include "Task.hpp"
#include <iostream>
#include <memory>
using namespace std;
#include <unistd.h>

int main()
{
    threadPool<culculateTask<int>>::getInstance()->start();
    printf("0x%x\n", threadPool<culculateTask<int>>::getInstance());
    int left = 0, right = 0;
    char oper;
    while (true)
    {
        cout << "Input left number# ";
        cin >> left;
        cout << "Input right number# ";
        cin >> right;
        cout << "Input oper# ";
        cin >> oper;
        culculateTask<int> task(left, right, oper);
        threadPool<culculateTask<int>>::getInstance()->push(task);
        printf("0x%x\n", threadPool<culculateTask<int>>::getInstance());
        usleep(1234);
    }
    return 0;
}

我们在测试用力当中使用了两句printf()语句,其目的就是证明对象永远最多只有一个:

1.2线程安全版本的懒汉方式

上面的代码已经实现单例了,但是我们并不满足,因为我们知道,多个执行流之间可能同时创建对象,如果我们不加安全保护,就会带来数据不一致问题(多创建对象)。那么线程安全的懒汉实现方式为:

    template <class T>
    class threadPool;

    template <class T>
    class threadData /*传递给线程例程的参数*/
    {
    public:
        threadPool<T> *_this;
        string _name;
    };

    template <class T>
    class threadPool
    {
    private:
#define INIT_THREAD_NUM 10
        static void *start_routine(void *args);

    private:
        void queueLock() { pthread_mutex_lock(&_mutex); }
        void queueUnlock() { pthread_mutex_unlock(&_mutex); }
        bool isQueueEmpty() { return _taskQueue.empty(); }
        void queueWait() { pthread_cond_wait(&_cond, &_mutex); }
        T taskPop();
        pthread_mutex_t *mutex() { return &_mutex; }

    private:
        /*将构造函数作为私有,并且禁用拷贝构造和赋值重载
         *目的就是为了不允许在类外定义对象*/
        threadPool(int num = INIT_THREAD_NUM);
        threadPool(const threadPool &tp) = delete;
        threadPool &operator=(const threadPool &tp) = delete;

    public:
        ~threadPool();
        void start();
        void push(const T &in);

        /*给定一个静态方法,用来获取单例对象的指针
         *静态方法可以不用对象访问*/
        static threadPool<T> *getInstance()
        {
            /*如果没有最外层的这条判断语句,那么每个线程要创建对象时都要加锁
             *如果在加锁之前告诉线程对象已经存在了,线程就不用再加锁了
             *从而减少加锁和解锁所带来的开销*/
            if (_singleton == nullptr)
            {
                /*如果单例指针为空,说明对象还没有被实例化过*/
                pthread_mutex_lock(&_singletonMutex);
                if (_singleton == nullptr)
                {
                    _singleton = new threadPool<T>();
                }
                pthread_mutex_unlock(&_singletonMutex);
            }
            return _singleton;
        }

    private:
        vector<Thread *> _threadVec;
        queue<T> _taskQueue;
        pthread_mutex_t _mutex;
        pthread_cond_t _cond;

        /*单例指针和多线程创建对象时的锁*/
        static threadPool<T> *_singleton;
        pthread_mutex_t _singletonMutex;
    };
    template <class T>
    threadPool<T> *threadPool<T>::_singleton = nullptr;

2.STL、智能指针与线程安全

实际上STL中的容器不是线程安全的,这就是为什么我们在写生产消费模型、线程池时,使用了容器还需要主动加锁的原因。因为STL的设计初衷是将性能挖掘到机制,而一旦涉及到线程安全,就会对性能造成巨大影响,所以在使用STL容器时,需要程序员自己控制STL容器的线程安全问题。

对于unique_ptr来说,它只在当前代码块范围内生效,因此不涉及线程安全问题。但是对于shared_ptr,多个对象需要共用一个引用计数变量,所以就会存在线程安全问题,但是标准库实现的时候考虑到了这个问题,因此使用了原子操作的方式操作引用计数,从而保证shared_ptr的高效,这种原子操作实际上就是CAS操作。

3.自旋锁

我们之前介绍的互斥锁是一种悲观锁,悲观锁的特点就是持有锁的执行流访问临界资源时,阻塞其他执行流对临界资源的访问。很显然,悲观锁降低了并发性能,但是却保证了数据一致和线程安全。

自旋锁是一种乐观锁,当持有自旋锁的执行流访问临界资源时,其他执行流不会被阻塞,而是一直在尝试申请锁,直到成功为止。因此,自旋锁适用于访问临界资源消耗的时间较少的场景,能够提高并发性能,减少线程的阻塞时间。

我们可以以一幅逻辑图来理解互斥锁与自旋锁的区别

也就是说,正在被访问的临界资源决定了其他线程需要等待,而访问临界的时间长短决定了线程的等待方式,即可以使用阻塞挂起等待和持续申请锁两种方式,这两种方式分别对应了互斥锁和自旋锁。需要注意的是,加锁和解锁的行为是程序员行为,所以选用互斥锁还是自旋锁完全由程序员决定,也就是说程序员有义务去预估访问临界资源所花费的时间再去选择合适的锁。当然,最笨的方法就是分别测试两个锁的时间,哪个消耗的时间短就用哪个锁。

在Linux当中,pthread原生线程库确实提供了有关自旋锁的接口,pthread_spin_t为自旋锁的类型:

  pthread_spin_init()和pthread_spin_destroy()负责自旋锁的初始化和析构。

  该接口与pthread_mutex_lock()非常相似,就不做过多介绍了,只需要知道申请自旋锁失败的线程不会陷入阻塞即可

至于自旋锁的使用方法,我只能说与互斥锁一摸一样,我们仅需要记住自旋锁和互斥锁的差别就可以了。当然,使用自旋锁的时候一定要小心,因为当访问临界资源的时间过长、出现死锁等问题的时候,申请自旋锁的线程不会挂起等待,而是一直在申请自旋锁,这就是会造成CPU资源浪费,如果情况较为极端,那么使用自旋锁是十分危险的动作。

4.读者写者问题

在多线程编程当中,有一种情况是十分常见的,即某些公共资源的被修改次数很少。这就意味着负责写入的线程竞争到公共资源的机会比较少,更多的是负责读取的线程竞争到公共资源的机会比较多。这样我们又可以讨论一个新的问题,即读者写者问题。

读者写者问题实际上可以归类为生产消费模型,但是读者与消费者的区别在于:消费者要从缓冲区拿走数据,读者将缓冲区的数据拷贝一份,即不拿走缓冲区的数据。读者写者的场景大多发生于发布某种数据、资源,该资源很长一段时间内不会被修改,更多的时间是在被读取。那么既然读者写者问题也可以归类为生产消费模型,那么它也需要遵循三二一原则

  1.保持三种关系:读者与写者之间互斥,即读者与写者不能同时操作"同一本书";写者与写者之间互斥,即统一时刻只允许一个写者"写书";读者与读者之间没有任何关系,读者想要"读书"时,不需要顾及其他读者的感受,因为它们不会把"书本"占为己有(读者不会从缓冲区拿走数据)。

  2.保持两种身份:即保证有读者和写者两种身份。

  3.保持一个缓冲区:这个缓冲区就是供读者读取的资源,写者要操作的资源。

综上所述,不难推断出读者写者问题是一种公共资源被多读少写的情况。如果使用互斥锁来控制共享资源的访问,就会导致读操作和写操作之间相互阻塞,从而降低并发性能;并且使用互斥锁很容易发生饥饿问题。那么在操作系统当中就有了读写锁这样的概念,读写锁就是为了解决多读少写的并发问题。当然,Linux的pthread原生线程库也提供了读写锁,pthread_rwlock_t就是读写锁的类型:

  这里的pthread_rwlock_rdlock()指的是申请读锁。

  pthread_rwlock_wrlock()指的是申请写锁。

  读锁、写锁统一使用pthread_rwlock_unlock()解锁。

此时就会产生一个问题,那就是读者与写者之间存在互斥、写者与写者之间存在互斥,那么它们两者之间直接竞争一把锁不久行了吗?为什么需要读、写两把锁?此时我们便要从伪代码的角度去分析读写锁的功能

可以看到读锁的作用就是为了保护里面的一个计数器,这个计数器用来表示当前有多少读者正在读取数据,这个计数器被多个读者之间共享、操作,所以它需要被保护;当读者申请到读锁的时候,证明当前读者可以读取数据,那么就让计数器++,如果当前读者是第一个读者,那么此时写者就不能写数据,这就是读者与写者之间的互斥关系;当读者读取数据之间释放了读锁,这就说明读者与读者之间不存在互斥关系;最后,当解锁读锁时,计数器会--,直到最后一个读者退出,此时写者可以写数据。

上面的伪代码是读者优先模式,读写锁的默认方式就是读者优先,因为读者优先,就有可能导致写者的饥饿问题(至少有一个读者永不退出)。事实上,写者优先模式不可能实现,但是Linux还是支持了这种模式的设置,但是会存在其他问题

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 写者优先,但写者不能递归加锁
*/

CPP 复制 全屏

既然这篇博客作为补充章节去进行介绍,所以有关读者写者的代码就不展示。事实上也没什么展示的,因为它的实验现象并不是很明显,我们只需要记住读者写者的使用的场景就行了。

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

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

相关文章

五一后“实在高校行”紧锣密鼓走进四所高校,校企合作硕果累累!

近年来&#xff0c;随着人工智能科技的快速发展&#xff0c;越来越多高校加快了与先进企业在培养优秀人才的合作步伐。实在智能一直注重校园生态发展及在人才培养&#xff0c;不断引进数字化人才&#xff0c;做强培训师资&#xff1b;同时积极走出去&#xff0c;在全社会范围内…

2023-05-20:go语言的slice和rust语言的Vec的扩容流程是什么?

2023-05-20&#xff1a;go语言的slice和rust语言的Vec的扩容流程是什么&#xff1f; 答案2023-05-20&#xff1a; go语言的slice扩容流程 go版本是1.20.4。 扩容流程见源码见runtime/slice.go文件中的growslice 函数。 growslice 函数的大致过程如下&#xff1a; 1.如果元…

Mybatis Plus之DQL(条件查询方式、查询投影、查询条件设定、字段映射与表名映射)

文章目录 1 条件查询1.1 条件查询的类1.2 环境构建1.3 构建条件查询1.4 多条件构建1.5 null判定 2 查询投影2.1 查询指定字段2.2 聚合查询2.3 分组查询 3 查询条件3.1 等值查询3.2 范围查询3.3 模糊查询3.4 排序查询 4 映射匹配兼容性问题1:表字段与编码属性设计不同步问题2:编…

你真的知道怎么使用vs吗?把把手教你实用调试小技巧

实用调试小技巧 1.什么是bug&#xff1f;2.调试是什么&#xff1f;有多重要&#xff1f;3.debug和release的介绍。4.windows环境调试介绍。4.1常见调试快捷键4.2 调试的时候查看程序当前信息4.2.1监视&#xff1a;4.2.2内存4.2.3调用堆栈4.2.4反汇编4.2.5寄存器 5.一些调试的实…

HNU-计算机系统-实验4-ShellLab

ShellLab 计科2102 梅炳寅 202108010206 写在前面 作为一份实验报告,我希望阅读者能够比较好地看到这份报告有价值的部分。私以为更为有价值的部分体现在: 报告中打★的部分,最后的代码中,我在代码中加入了大量的中文注释、函数原型、参数解读、以及个人对代码的推断与理…

网易云音乐开发--音乐播放暂停切换上下首功能实现

音乐播放暂停功能实现 封装一个控制音乐播放/暂停的功能函数 看一下文档&#xff0c;我需要用的api 这个接口好像没有音频的url&#xff0c;查看一下&#xff0c;换个api 这样就能拿到id&#xff0c;并可以播放了 但是音乐并没有播放 我们少了这个 现在可以播放了&#xff…

[CTF/网络安全] 攻防世界 view_source 解题详析

[CTF/网络安全] 攻防世界 view_source 解题详析 查看页面源代码方式归类总结 题目描述&#xff1a;X老师让小宁同学查看一个网页的源代码&#xff0c;但小宁同学发现鼠标右键好像不管用了。 查看页面源代码方式归类 单击鼠标右键&#xff0c;点击查看页面源代码&#xff1a; …

Linux 指令(三)+完整思维导图+实图例子+深入细节+通俗易懂建议收藏

绪论 涓滴之水终可磨损大石&#xff0c;不是由于它的力量强大&#xff0c;而是由于昼夜不舍的滴坠。今天我们继续学习Linux指令。 话不多说安全带系好&#xff0c;发车啦&#xff08;建议电脑观看&#xff09;。 附&#xff1a;红色&#xff0c;部分为重点部分&#xff1b;蓝颜…

由斯坦福、Nautilus Chain等联合主办的 Hackathon 活动,现已接受报名

由 Stanford Blockchain Accelerator、Zebec Protocol、 Nautilus Chain、Rootz Lab 共同主办的黑客松活动&#xff0c;现已接受优秀项目提交参赛申请。 在加密行业发展早期&#xff0c;密码极客们就始终在对区块链世界基础设施&#xff0c;在发展方向的无限可能性进行探索。而…

四、easyUI中的tabs(选项卡)组件

1.tabs&#xff08;选项卡&#xff09;组件的概述 选项卡会显示一批面板&#xff0c;但在同一个时间只会显示一个面板。每个选项卡面板都有头标题和一些小的按钮工具菜单&#xff0c;包括关闭按钮和其他自定义按钮。 2.tabs&#xff08;选项卡&#xff09;组件的使用 在项目新…

【C++】——内存管理(new和delete)

文章目录 1. 前言2. C/C内存分布3. C语言动态内存管理方式4. C内存管理方式4.1 内置类型4.2 自定义类型 5. operator new与operator delete函数6. new和delete的实现原理7. 定位new表达式(placement-new)8. 结尾 1. 前言 在学习C语言的时候&#xff0c;我们学习了动态内存管理…

物联网协议之MQTT

MQTT 简介 MQTT 可以简单看做一个网络协议&#xff0c;用于机器对机器的通信&#xff08;与客户端到服务器的传输有点区别&#xff09;。智能传感器、可穿戴设备和其他物联网&#xff08;IoT&#xff09;设备通常必须通过带宽有限的资源受限网络传输和接收数据。这些物联网设…

更新cuda和 pytorch==1.12.1版本,更新到cuda11.3.1

nvidia-smi 查看gpu支持的最高cuda版本 nvcc -V 查看当前cuda版本 卸载旧版本cuda 除了NVIDIA Geforce、NVIDIA PhysX、NVIDIA图形驱动程序&#xff0c;将电脑中其他NVIDIA开头的全部卸载 安装cuda 下载适合的cuda版本 https://developer.nvidia.com/cuda-toolkit-ar…

基于QT开发的使用OPC_UA与西门子1200,1500系列PLC通信的工业监控Demo

目录 一&#xff0c;总体介绍 二&#xff0c;需要的软件 三&#xff0c;需要的硬件 四&#xff0c;QT程序代码 五&#xff0c;西门子PLC代码 一&#xff0c;总体介绍 先看一下图1-1的QT运行界面图&#xff0c;界面中服务器地址就是OPC_UA服务器地址&#xff0c;整个项目作…

图解LeetCode——19. 删除链表的倒数第 N 个结点

一、题目 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 二、示例 2.1> 示例 1&#xff1a; 【输入】head [1,2,3,4,5], n 2 【输出】[1,2,3,5] 2.2> 示例 2&#xff1a; 【输入】head [1], n 1 【输出】[] 2.3> 示例…

数值计算 - 误差的来源

误差的来源是多方面的&#xff0c;但主要来源为&#xff1a;过失误差&#xff0c;描述误差&#xff0c;观测误差&#xff0c;截断误差和舍入误差。 过失误差 过失误差是由设备故障和人为的错误所产生的误差&#xff0c;在由于每个人都有“权利”利用机器进行数值计算,所以在计算…

#include <archive.h>报错

#include <archive.h>报错 archive配置 Linux环境下&#xff0c;在C项目.cpp文件中存在如下语句&#xff0c;导致无法运行~ #include <archive.h> #include <archive_entry.h>由于C编译器默认不包含archive&#xff0c;所以我们需要自行配置。 Libarchiv…

Java 基础--interview经典(个人认为)题目

1&#xff09;多线程中 synchronized 锁升级的原理是什么&#xff1f; synchronized 锁升级原理&#xff1a;在锁对象的对象头里面有一个 threadid 字段&#xff0c;在第一次访问的时候 threadid 为空&#xff0c;jvm 让其持有偏向锁&#xff0c;并将 threadid 设置为其线程 i…

终の序章(一)

前言 写在我大三的最后一次省赛 &#xff0c;也是最后一次比赛 从半年前区域赛的大失利&#xff0c;导致我曾一度放弃算法竞赛和算法训练 新路历程 考虑到前几次的省赛&#xff0c;我们这次采取 稳 的一种方式&#xff0c;因为前几次省赛难度跨度较 大&#xff0c;对于大部…

【牛客网面试必刷TOP101】链表篇(二)

【牛客网面试必刷TOP101】链表篇&#xff08;二&#xff09; 前言刷题网站刷题&#xff01;BM4 合并两个排序的链表思路一&#xff1a;双指针思路二&#xff1a;递归&#xff08;扩展思路&#xff09; BM5 合并k个已排序的链表思路一&#xff1a;归并排序思想 BM6 判断链表中是…