[Linux]多线程的同步和互斥(线程池 | 单例模式 | 其他常见的锁 | 读者写者问题)

news2024/12/24 21:07:37

在这里插入图片描述

文章目录

  • 线程池
  • 线程安全的单例模式
    • 单例模式的特点
    • 饿汉方式和懒汉方式
  • 单例模式实现线程池
  • 其他常见的锁
  • 读者写者问题

线程池

线程池是一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的运用场景:

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

设计一个线程池:

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

代码实现:

Makefile文件:

thraed_pool:main.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -rf thraed_pool

thread_pool.hpp文件:

#pragma once
#include <iostream>
#include <queue>
#include <string>
#include <pthread.h>

// 基于工作队列的线程池
namespace ns_thread_pool
{
    const int g_num = 10;
    template <class T>
    class ThreadPool
    {
    private:
        std::queue<T> _task_queue; // 工作队列
        int _num;                  // 工作队列的容量上限
        pthread_mutex_t _mtx;      // 保护临界资源的互斥量
        pthread_cond_t _cond;

    public:
        void Lock()
        {
            pthread_mutex_lock(&_mtx);
        }

        void Unlock()
        {
            pthread_mutex_unlock(&_mtx);
        }

        void Wakeup()
        {
            pthread_cond_signal(&_cond);
        }

        void Wait()
        {
            pthread_cond_wait(&_cond, &_mtx);
        }

        bool IsEmpty()
        {
            return _task_queue.empty();
        }

    public:
        ThreadPool(int num = g_num) : _num(num)
        {
            pthread_mutex_init(&_mtx, nullptr);
            pthread_cond_init(&_cond, nullptr);
        }

        static void *Rountine(void *args)
        {
            pthread_detach(pthread_self());
            ThreadPool<T> *tp = (ThreadPool<T> *)args;
            while (true)
            {
                tp->Lock();
                while (tp->IsEmpty())
                {
                    tp->Wait();
                }
                T t;
                tp->PopTask(&t);
                tp->Unlock();

                t();
            }
        }

        void ThreadInit()
        {
            pthread_t tid;
            for (int i = 0; i < _num; i++)
            {
                pthread_create(&tid, nullptr, Rountine, (void *)this);
            }
        }

        void PushTask(const T &in)
        {
            Lock();
            _task_queue.push(in);
            Unlock();
            Wakeup();
        }

        void PopTask(T *out)
        {
            *out = _task_queue.front();
            _task_queue.pop();
        }

        ~ThreadPool()
        {
            pthread_mutex_destroy(&_mtx);
            pthread_cond_destroy(&_cond);
        }
    };
}

Task.hpp文件:

#pragma once
#include <iostream>
#include <string>
#include <pthread.h>

namespace ns_task
{
    class Task
    {
    private:
        int _x;
        int _y;
        char _op;
    public:
        Task() {}
        Task(int x, int y, char op):_x(x), _y(y), _op(op) {}
        std::string message()
        {
            std::string msg = std::to_string(_x);
            msg += _op;
            msg += std::to_string(_y);
            msg += "=?";

            return msg;
        }
        int Run()
        {
            int res = 0;
            switch(_op)
            {
            case '+':
                res = _x + _y;
                break;
            case '-':
                res = _x - _y;
                break;
            case '*':
                res = _x * _y;
                break;
            case '/':
                res = _x / _y;
                break;
            case '%':
                res = _x % _y;
                break;
            default:
                break;
            }
            std::cout << "Thread: " << pthread_self() << " Task: " << _x << _op << _y << "=" << res << std::endl;
            return res;
         }
        
        int operator()()
        {
            return Run();
        }

        ~Task(){}
    };
}

main.cc文件:

#include "thread_pool.hpp"
#include "Task.hpp"
#include <ctime>
#include <cstdlib>
#include <unistd.h>

using namespace ns_task;
using namespace ns_thread_pool;

int main()
{
    srand((long long)time(nullptr));
    ThreadPool<Task>* tp = new ThreadPool<Task>();
    tp->ThreadInit();
    while(true)
    {
        Task t(rand()%20+1, rand()%10+1, "+-*/%"[rand()%5]);
        tp->PushTask(t);
    }

    return 0;
}

运行结果:

[cwx@VM-20-16-centos normal_thread_pool]$ make
g++ -o thraed_pool main.cc -std=c++11 -lpthread
[cwx@VM-20-16-centos normal_thread_pool]$ ./thraed_pool 
Thread: 140412644165376 Task: 10-7=3
Thread: 140412644165376 Task: 17+10=27
Thread: 140412669343488 Task: 6-7=-1
Thread: 140412711307008 Task: 2/6=0
Thread: 140412702914304 Task: 10/7=1
......

线程安全的单例模式

单例模式的特点

  • 某些类, 只应该具有一个对象(实例), 就称之为单例.
  • 在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.

饿汉方式和懒汉方式

洗碗的例子解释饿汉方式和懒汉方式:

  • 吃完饭立刻洗碗,就是饿汉方式,这样下一次就可以立即拿着碗吃饭
  • 吃完饭不立刻洗碗,这就是懒汉方式,下一次用到再洗碗吃饭

懒汉方式最核心的思想就是"延时加载",从而优化服务器的启动速度。

饿汉方式实现单例模式:

template <typename T>
class Singleton {
	static T data;
public:
	static T* GetInstance() {
		return &data;
	}
};

只要通过 Singleton这个类来使用 T 对象, 则一个进程中只有一个 T 对象的实例.

懒汉方式实现单例模式:

template <typename T>
class Singleton {
	static T* inst;
public:
	static T* GetInstance() {
		if (inst == NULL) {
			inst = new T();
		} 
		return inst;
	}
};

存在一个严重的问题, 线程不安全.
第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例.

饿汉方式实现单例模式(线程安全版本):

template <typename T>
class Singleton {
	volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
	static std::mutex lock;
public:
	static T* GetInstance() {
	if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提高性能.
		lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
		if (inst == NULL) {
		inst = new T();
		} 
		lock.unlock();
	} 
	return inst;
	}
};

注意事项:

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

单例模式实现线程池

thread_pool.hpp文件:

#pragma once

#include <iostream>
#include <queue>
#include <string>
#include <pthread.h>

// 懒汉模式单例模式多线程池
namespace ns_thread_pool
{
    const int g_num = 10;

    template <class T>
    class ThreadPool
    {
    private:
        std::queue<T> _task_queue;   // 工作队列 -- 临界资源
        int _num;                    // 工作队列的容量
        pthread_mutex_t _mtx;        // 保护工作队列临界资源的锁
        pthread_cond_t _cond;        // 队列为空,在此环境变量等待

        static ThreadPool<T>* inst;  // 单例模式的静态对象指针

    private:
        // 单例模式构造函数必须定义以及必须为私有private
        ThreadPool(int num = g_num) : _num(num)
        {
            // 互斥锁和环境变量初始化
            pthread_mutex_init(&_mtx, nullptr);
            pthread_cond_init(&_cond, nullptr);
        }

        ThreadPool(const ThreadPool<T> &tp) = delete;
        ThreadPool<T> &operator=(ThreadPool<T> &tp) = delete;

    public:
        void Lock()
        {
            pthread_mutex_lock(&_mtx);
        }

        void Unlock()
        {
            pthread_mutex_unlock(&_mtx);
        }

        void Wakeup()
        {
            pthread_cond_signal(&_cond);
        }

        void Wait()
        {
            pthread_cond_wait(&_cond, &_mtx);
        }

        bool IsEmpty()
        {
            return _task_queue.empty();
        }

    public:
        static ThreadPool<T> *GetInstance()
        {
            // 静态互斥锁初始化
            static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
            // 双条件判断可以减少锁的争用,提高效率
            if(inst == nullptr)
            {
                pthread_mutex_lock(&lock);
                if(inst == nullptr)
                {
                    inst = new ThreadPool<T>();
                    inst->ThreadInit();
                }
                pthread_mutex_unlock(&lock);
            }

            return inst;
        }

        static void *Rountine(void *args)
        {
            pthread_detach(pthread_self());  // 线程分离,主线程无需关注该线程
            ThreadPool<T> *tp = (ThreadPool<T> *)args;
            while (true)
            {
                tp->Lock();
                while (tp->IsEmpty())
                {
                    // 工作队列为空,线程需要等待
                    tp->Wait();
                }
                T t;
                tp->PopTask(&t);
                tp->Unlock();

                t();
            }
        }

        void ThreadInit()
        {
            // 创建_num个线程的多线程池
            pthread_t tid;
            for (int i = 0; i < _num; i++)
            {
                pthread_create(&tid, nullptr, Rountine, (void *)this);
            }
        }

        void PushTask(const T &in)
        {
            Lock();
            _task_queue.push(in);
            Unlock();
            Wakeup();
        }

        void PopTask(T *out)
        {
            *out = _task_queue.front();
            _task_queue.pop();
        }

        ~ThreadPool()
        {
            pthread_mutex_destroy(&_mtx);
            pthread_cond_destroy(&_cond);
        }
    };

    template<class T>
    ThreadPool<T>* ThreadPool<T>::inst = nullptr;
}

其他常见的锁

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

挂起等待特性的锁 vs 自旋锁

example:小明准备和女朋友小红约会,小明来到小红的宿舍楼下给小红打电话问小红何时下楼,小红表示需要化个妆才能约会需要半个小时的时间,小明听后前往网吧边打游戏边等待小红。如果小红说无需化妆只需要五分钟就可以下楼,小明就不会前往网吧,而是在楼下每一分钟发消息询问小红的下楼情况。
小红如果需要化妆,小明前往网吧的情况等价于挂起等待。
小红无需化妆,小明在楼下不断发消息,检测小红的状态等价于自旋的过程。

决定小明是否前往网吧的因素是小红需要多长时间才能下楼,如果小红需要化妆就前往网吧,等价于线程挂起等待,无需化妆就不断进行检测,等待于自旋的过程,这是因为线程挂起等待是有成本的。

线程如果访问临界资源花费的时长比较短,比较适合自旋锁:

#include <pthread.h>

int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);

线程如果访问临界资源花费的时长比较长,比较适合挂起等待性质的锁,比如悲观锁。


读者写者问题

  • 在读者写者模型中,对数据的大部分操作是读取,少部分操作是写入。
  • 进行数据读取的一端如果不会将数据取走,就可以考虑读者写者模型。

在这里插入图片描述

321原则:

三种关系:

  • 读者 vs 读者:无关系
  • 写者 vs 写者:互斥关系
  • 读者 vs 写者:同步、互斥关系

两种角色:

  • 读者
  • 写者

一个交易场所:

  • 一段缓冲区

基本操作:

设置读者优先:

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

初始化:

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

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

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

相关文章

K26 SOM从emmc启动linux——2

问题 在K26 SOM从emmc启动linux文章中&#xff0c;使用EMMC启动后&#xff0c;出现下图打印&#xff1a; 这里本身是存在问题的&#xff0c;没有显示系统名称&#xff0c;打印中有错误提示。 解决办法 在Image Packaging Configuration中将petalinux-initramfs-image修改成p…

教你快速实现“基于Docker快速构建基于Prometheus的MySQL监控系统”

&#x1f341; 个人网站&#xff1a;知识浅谈 &#x1f4cc; 资料分享群&#xff1a;资源分享 &#x1f356;400多套PPT模板&#xff1a;商务模板&学生答辩 &#x1f91e;这次都给他拿下&#x1f91e; 基于Docker快速构建基于Prometheus的MySQL监控系统 先来捋一下数据流的…

Java Lambda表达式原理解析

前言 java中有lambda,在使用java的过程中我们没少用(啥?用的kotlin?你别说话)但是你知道lambda的实现原理吗? 接下来就来解析lambda实现,不过在lambda之前我们与一个熟悉的老伙计谈谈心————匿名类,为什么因为他们有点类似. 匿名类的实现方式 从字节码的层面上来说ne…

长光卫星冲刺科创板上市,预计2025年底前实现300颗卫星在轨

近日&#xff0c;长光卫星技术股份有限公司&#xff08;下称“长光卫星”&#xff09;在上海证券交易所递交招股书&#xff0c;准备在科创板上市。 本次冲刺科创板上市&#xff0c;长光卫星计划募资26.83亿元&#xff0c;将用于“吉林一号”卫星星座建设项目&#xff08;二期&a…

第五章:数据完整性

一、实体、参照、用户自定义完整性 1、【单选题】CREATE TABLE时定义属性上的约束条件&#xff0c;检查列值是否满足一个条件表达式的定义关键词为&#xff1a; 正确答案&#xff1a; A 2、【多选题】创建患者住院主记录表pat_visit&#xff0c;并定义主码{patient_id,visit_…

《图解TCP/IP》阅读笔记(第七章 7.1、7.2、7.3)—— 路由控制概念与路由控制算法

第七章 路由协议 本章旨在将详细介绍路由控制以及实现路由控制功能的相关协议 7.1 路由控制的定义 在互联网这片汪洋大海中&#xff0c;数据就好似一叶扁舟&#xff0c;没有灯塔的指引&#xff0c;是难以寻得目的地的。这种进行正确方向引导的转发数据的处理&#xff0c;就叫…

[LeetCode周赛复盘] 第 94 场双周赛20221225

[LeetCode周赛复盘] 第 94 场双周赛20221225 一、本周周赛总结二、 [Easy] 6273. 最多可以摧毁的敌人城堡数目1. 题目描述2. 思路分析3. 代码实现三、[Medium] 6274. 奖励最顶尖的 K 名学生1. 题目描述2. 思路分析3. 代码实现四、[Medium] 6295. 最小化两个数组中的最大值1. 题…

从入门到项目实战 - Vue 键盘事件

Vue 中键盘事件的使用上一节&#xff1a;《 Vue 事件处理 》| 下一节&#xff1a;《 Vue 中鼠标事件的使用 》jcLee95 邮箱 &#xff1a;291148484163.com CSDN 主页&#xff1a;https://blog.csdn.net/qq_28550263?spm1001.2101.3001.5343 本文地址&#xff1a;https:/…

【TypeScript】TS泛型讲解及其使用

目录 泛型 泛型函数使用 泛型约束 多泛型使用 泛型接口 泛型类 泛型工具类型 泛型 泛型是可以在保证类型安全的前提下&#xff0c;让函数等与多种类型一起工作&#xff0c;从而实现复用&#xff0c;常用于&#xff1a;函数、接口、class中。日常我们创建的函数&#xf…

车牌检测模型训练(含源码和数据集)

车牌检测模型训练(含源码和数据集) 本教程利用NVIDIA TAO进行车牌检测模型的训练: 模型框架:SSD数据集: CRPD, 连接:https://github.com/yxgong0/CRPD训练框架: NVIDIA TAO, 安装教程连接: https://docs.nvidia.com/tao/tao-toolkit/text/tao_toolkit_quick_start_guide.html…

目标检测之YOLOv3算法分析

基本原理 特征网络 输入输出 输入416∗416∗3416*416*3416∗416∗3大小的图片&#xff08;不唯一&#xff0c;但图片大小必为32的倍数&#xff09;&#xff0c;输出3个尺度的feature map,分别为13∗13∗25513*13*25513∗13∗255&#xff0c;26∗26∗25526*26*25526∗26∗255…

RV1126笔记十九:吸烟行为检测及部署<六>

若该文为原创文章,转载请注明原文出处。 模型测试 一、pt文件测试 pt文件是在windows下训练生成的,测试环境为py3.8 1、首先查看虚拟环境列表,然后切换于是py3.8的虚拟环境。 conda env list // 查看虚拟环境列表 conda activate yolo // 切换虚拟环境 2…

leetcode:1739. 放置盒子【找规律!】

目录题目截图题目分析ac code总结题目截图 题目分析 样例有规律&#xff0c;它希望我们先按每层1, 3, 6这样叠起来&#xff0c;比如能跌i层那么至少有i * ( i 1) // 2个底层多出来的东西再做考虑多出来的东西考虑1 2 3…能加到多少个比如说如果多出来3个的话&#xff0c;放…

【小5聊】Asp.Net Core3.1基础之跨域设置以及设置不对的地方

最近微软的.Net Core平台更新换代速度非常快&#xff0c;还没把2.1整熟悉&#xff0c;就把2.1淘汰了。 目前最新版本已经到了7.0&#xff0c;.net core3.1还在长期维护范围内&#xff0c;估计能用一段时间。 所以&#xff0c;.net core2.1升级到3.1&#xff0c;跨域方法的设置也…

gerber 文件格式 [一]

在电路设计这块, 目前还绕不开 gerber 文件的工程交互, 所以来了解一下. 目前官网的文档gerber-layer-format-specification-revision-2022-02_en.pdf. gerber 文件是一个ascii码的命令文档, 格式比较简单,主要命令有下面这些 命令名称说明G04注释对文档生成没有影响MO模式设…

基于天鹰算法改进的DELM预测-附代码

天鹰算法改进的深度极限学习机DELM的回归预测 文章目录天鹰算法改进的深度极限学习机DELM的回归预测1.ELM原理2.深度极限学习机&#xff08;DELM&#xff09;原理3.天鹰算法4.天鹰算法改进DELM5.实验结果6.参考文献7.Matlab代码1.ELM原理 ELM基础原理请参考&#xff1a;https:…

一文弄懂 React HOC

1. 提出问题 1.HOC 能解决什么问题&#xff1f; 2.HOC 的使用场景&#xff1f; 2. HOC 能解决什么问题&#xff1f; 1.拦截组件渲染&#xff0c;包括是否渲染组件、懒加载组件 2.往组件的 props 中混入所需的东西&#xff0c;比如给非 Route 组件的 props 混入 history 对象…

node.js+uni计算机毕设项目交流微信小程序LW(程序+小程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等…

信号量和共享内存

信号量 信号量(Semaphore)&#xff0c;有时被称为信号灯&#xff0c;是在多线程环境下使用的一种设施&#xff0c;是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前&#xff0c;线程必须获取一个信号量;一旦该关键代码段完成了&#xff0c;那么该线…

Joplin插件推荐-持续更新

背景 之前因为印象笔记、语雀等笔记软件使用起来都不满足自己的需求&#xff0c;所以后面自己调研后使用了Joplin这个开源笔记软件 &#xff0c;项目主页&#xff1a; https://joplinapp.org 。目前搭建在自己的服务器上。最近发现有很多好用的插件。所以记录分享一下。 总插…