Linux知识点 -- Linux多线程(四)

Linux知识点 – Linux多线程(四)

文章目录

  • Linux知识点 -- Linux多线程(四)
  • 一、线程池
    • 1.概念
    • 2.实现
    • 3.单例模式的线程池
  • 二、STL、智能指针和线程安全
    • 1.STL的容器是否是线程安全的
    • 2.智能指针是否是线程安全的
  • 三、其他常见的各种锁
  • 四、读者写者问题
    • 1.读写锁
    • 2.读写锁接口


一、线程池

1.概念

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

  • 预先申请资源,用空间换时间;
  • 预先申请一批线程,任务到来就处理;
  • 线程池就是一个生产消费模型;

2.实现

thread.hpp
线程封装:

#pragma once

#include<iostream>
#include<string>
#include<functional>
#include<cstdio>

typedef void* (*fun_t)(void*); // 定义函数指针类型,后面回调

class ThreadData  // 线程信息结构体
{
public:
    void* _args;
    std::string _name;
};

class Thread
{
public:
    Thread(int num, fun_t callback, void* args)
        : _func(callback)
    {
        char nameBuffer[64];
        snprintf(nameBuffer, sizeof(nameBuffer), "Thread-%d", num);
        _name = nameBuffer;

        _tdata._args = args;
        _tdata._name = _name;
    }

    void start() // 创建线程
    {
        pthread_create(&_tid, nullptr, _func, (void*)&_tdata); // 直接将_tdata作为参数传给回调函数
    }

    void join() // 线程等待
    {
        pthread_join(_tid, nullptr);
    }

    std::string name()
    {
        return _name;
    }

    ~Thread()
    {}

private:
    std::string _name;
    fun_t _func;
    ThreadData _tdata;
    pthread_t _tid;
};

lockGuard.hpp
锁的封装,构建对象时直接加锁,对象析构时自动解锁;

#pragma once

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

class Mutex
{
public:
    Mutex(pthread_mutex_t *mtx)
        : _pmtx(mtx)
    {
    }

    void lock()
    {
        pthread_mutex_lock(_pmtx);
    }

    void unlock()
    {
        pthread_mutex_unlock(_pmtx);
    }

    ~Mutex()
    {}

private:
    pthread_mutex_t *_pmtx;
};


class lockGuard
{
public:
    lockGuard(pthread_mutex_t *mtx)
        : _mtx(mtx)
    {
        _mtx.lock();
    }

    ~lockGuard()
    {
        _mtx.unlock();
    }
private:
    Mutex _mtx;
};

log.hpp

#pragma once

#include<iostream>
#include<cstdio>
#include<cstdarg>
#include<ctime>
#include<string>

//日志级别
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

const char* gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

#define LOGFILE "./threadpool.log"

//完整的日志功能,至少需要:日志等级 时间 支持用户自定义(日志内容,文件行,文件名)

void logMessage(int level, const char* format, ...)
{
#ifndef DEBUG_SHOW
    if(level == DEBUG) return;
#endif

    char stdBuffer[1024];//标准部分
    time_t timestamp = time(nullptr);
    snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);

    char logBuffer[1024];//自定义部分
    va_list args;
    va_start(args, format);
    vsnprintf(logBuffer, sizeof(logBuffer), format, args);
    va_end(args);

    FILE* fp = fopen(LOGFILE, "a");
    fprintf(fp, "%s %s\n", stdBuffer, logBuffer);
    fclose(fp);
}
  • 注:
    (1)提取可变参数
    在这里插入图片描述
    使用宏来提取可变参数:
    在这里插入图片描述
    将可变参数格式化打印到对应地点:
    在这里插入图片描述
    format是打印的格式;
    在这里插入图片描述
    (2)条件编译:
    在这里插入图片描述
    条件编译,不想调试的时候,就不加DEBUG宏,不打印日志信息;
    在这里插入图片描述
    -D:在命令行定义宏 ;

threadPool.hpp

线程池封装:

#include "thread.hpp"
#include <vector>
#include <queue>
#include <unistd.h>
#include "log.hpp"
#include "Task.hpp"
#include "lockGuard.hpp"

const int g_thread_num = 3;

template <class T>
class ThreadPool
{
public:
    pthread_mutex_t *getMutex()
    {
        return &_lock;
    }

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

    void waitCond()
    {
        pthread_cond_wait(&_cond, &_lock);
    }

    T getTask()
    {
        T t = _task_queue.front();
        _task_queue.pop();
        return t;
    }

    ThreadPool(int thread_num = g_thread_num)
        : _num(thread_num)
    {
        pthread_mutex_init(&_lock, nullptr);
        pthread_cond_init(&_cond, nullptr);
        for (int i = 1; i <= _num; i++)
        {
            _threads.push_back(new Thread(i, routine, this));
            // 线程构造传入的this指针,是作为ThreadData结构体的参数的,ThreadData结构体才是routine回调函数的参数
        }
    }

    void run()
    {
        for (auto &iter : _threads)
        {
            iter->start();
            logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");
        }
    }

    // 消费过程:线程调用回调函数取任务就是所谓的消费过程,访问了临界资源,需要加锁
    static void *routine(void *args)
    {
        ThreadData *td = (ThreadData *)args;
        ThreadPool<T> *tp = (ThreadPool<T> *)td->_args; // 拿到this指针
        while (true)
        {
            T task;
            {
                lockGuard lockguard(tp->getMutex());
                while (tp->isEmpty())
                {
                    tp->waitCond();
                }
                // 读取任务
                task = tp->getTask();
                // 任务队列是共享的,将任务从共享空间,拿到私有空间
            }
            task(td->_name); // 处理任务
        }
    }

    void pushTask(const T &task)
    {
        lockGuard lockguard(&_lock); // 访问临界资源,需要加锁
        _task_queue.push(task);
        pthread_cond_signal(&_cond); // 推送任务后,发送信号,让进程处理
    }

    ~ThreadPool()
    {
        for (auto &iter : _threads)
        {
            iter->join();
            delete iter;
        }
        pthread_mutex_destroy(&_lock);
        pthread_cond_destroy(&_cond);
    }

private:
    std::vector<Thread *> _threads; // 线程池
    int _num;
    std::queue<T> _task_queue; // 任务队列
    pthread_mutex_t _lock;     // 锁
    pthread_cond_t _cond;      // 条件变量
};
  • 注:
    (1)如果回调函数routine放在thread类里面,由于成员函数会默认传this指针,因此参数识别的时候可能会出错,所以需要设置成静态成员;在这里插入图片描述
    在这里插入图片描述
    (2)如果设置成静态类内方法,这个函数只能使用静态成员,而不能使用其他类内成员;
    可以让routine函数拿到整体对象,在构造线程的时候,routine的参数传入this指针;

    在这里插入图片描述
    在构造函数的初始化列表中是参数的初始化,在下面的函数体中是赋值的过程,因此在函数体中对象已经存在了,就可以使用this指针了;
    (3)类内公有接口让静态成员函数routine通过this指针能够访问类内成员;
    在这里插入图片描述
    testMain.cc
#include"threadPool.hpp"
#include"Task.hpp"
#include<ctime>
#include<cstdlib>
#include<iostream>
#include<unistd.h>

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid());

    ThreadPool<Task>* tp = new ThreadPool<Task>();
    tp->run();

    while(true)
    {
        //生产的时候,只做任务要花时间
        int x = rand()%100 + 1;
        usleep(7756);
        int y = rand()%30 + 1;
        Task t(x, y, [](int x, int y)->int{
            return x + y;
        });

        logMessage(DEBUG, "制作任务完成:%d+%d=?", x, y);

        //推送任务到线程池中
        tp->pushTask(t);

        sleep(1);
    }

    return 0;
}

运行结果:
在这里插入图片描述

3.单例模式的线程池

threadPool.hpp

#include "thread.hpp"
#include <vector>
#include <queue>
#include <unistd.h>
#include "log.hpp"
#include "Task.hpp"
#include "lockGuard.hpp"

const int g_thread_num = 3;

template <class T>
class ThreadPool
{
public:
    pthread_mutex_t *getMutex()
    {
        return &_lock;
    }

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

    void waitCond()
    {
        pthread_cond_wait(&_cond, &_lock);
    }

    T getTask()
    {
        T t = _task_queue.front();
        _task_queue.pop();
        return t;
    }

//单例模式线程池:懒汉模式
private:
    //构造函数设为私有
    ThreadPool(int thread_num = g_thread_num)
        : _num(thread_num)
    {
        pthread_mutex_init(&_lock, nullptr);
        pthread_cond_init(&_cond, nullptr);
        for (int i = 1; i <= _num; i++)
        {
            _threads.push_back(new Thread(i, routine, this));
            // 线程构造传入的this指针,是作为ThreadData结构体的参数的,ThreadData结构体才是routine回调函数的参数
        }
    }

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

public:
    //创建单例对象的类内静态成员函数
    static ThreadPool<T>* getThreadPool(int num = g_thread_num)
    {
        //在这里再加上一个条件判断,可以有效减少未来必定要进行的加锁检测的问题
        //拦截大量的在已经创建好单例的时候,剩余线程请求单例而直接申请锁的行为
        if(nullptr == _thread_ptr)
        {
            //加锁
            lockGuard lockguard(&_mutex);
            //未来任何一个线程想要获取单例,都必须调用getThreadPool接口
            //一定会存在大量的申请锁和释放锁的行为,无用且浪费资源
            if(nullptr == _thread_ptr)
            {
                _thread_ptr = new ThreadPool<T>(num);
            }
        }
        return _thread_ptr;
    }

    void run()
    {
        for (auto &iter : _threads)
        {
            iter->start();
            logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");
        }
    }

    // 消费过程:线程调用回调函数取任务就是所谓的消费过程,访问了临界资源,需要加锁
    static void *routine(void *args)
    {
        ThreadData *td = (ThreadData *)args;
        ThreadPool<T> *tp = (ThreadPool<T> *)td->_args; // 拿到this指针
        while (true)
        {
            T task;
            {
                lockGuard lockguard(tp->getMutex());
                while (tp->isEmpty())
                {
                    tp->waitCond();
                }
                // 读取任务
                task = tp->getTask();
                // 任务队列是共享的,将任务从共享空间,拿到私有空间
            }
            task(td->_name); // 处理任务
        }
    }

    void pushTask(const T &task)
    {
        lockGuard lockguard(&_lock); // 访问临界资源,需要加锁
        _task_queue.push(task);
        pthread_cond_signal(&_cond); // 推送任务后,发送信号,让进程处理
    }

    ~ThreadPool()
    {
        for (auto &iter : _threads)
        {
            iter->join();
            delete iter;
        }
        pthread_mutex_destroy(&_lock);
        pthread_cond_destroy(&_cond);
    }

private:
    std::vector<Thread *> _threads; // 线程池
    int _num;
    std::queue<T> _task_queue; // 任务队列

    static ThreadPool<T>* _thread_ptr;
    static pthread_mutex_t _mutex;

    pthread_mutex_t _lock;     // 锁
    pthread_cond_t _cond;      // 条件变量
};

//静态成员在类外初始化
template<class T>
ThreadPool<T>* ThreadPool<T>::_thread_ptr = nullptr;

template<class T>
pthread_mutex_t ThreadPool<T>::_mutex = PTHREAD_MUTEX_INITIALIZER;

在这里插入图片描述
在这里插入图片描述
多线程同时调用单例过程,由于创建过程是非原子的,有可能被创建多个对象,是非线程安全的;
需要对创建对象的过程加锁,就可以保证在多线程场景当中获取单例对象;
但是未来任何一个线程想调用单例对象,都必须调用这个成员函数,就会存在大量申请和释放锁的行为;
可以在之间加一个对单例对象指针的判断,若不为空,就不进行对象创建;

在这里插入图片描述
testMain.cc

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

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid());

    //ThreadPool<Task>* tp = new ThreadPool<Task>();
    //tp->run();    
    ThreadPool<Task>::getThreadPool()->run();//创建单例对象

    

    while(true)
    {
        //生产的时候,只做任务要花时间
        int x = rand()%100 + 1;
        usleep(7756);
        int y = rand()%30 + 1;
        Task t(x, y, [](int x, int y)->int{
            return x + y;
        });

        logMessage(DEBUG, "制作任务完成:%d+%d=?", x, y);

        //推送任务到线程池中
        ThreadPool<Task>::getThreadPool()->pushTask(t);

        sleep(1);
    }

    return 0;
}

运行结果:
在这里插入图片描述

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

1.STL的容器是否是线程安全的

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

2.智能指针是否是线程安全的

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

三、其他常见的各种锁

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

    在这里插入图片描述
    互斥锁是检测到资源未就绪,就挂起线程;
    临界资源就绪的时间决定了使用哪种锁;

四、读者写者问题

1.读写锁

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

  • 读者写者模型与生产消费模型的本质区别:
    生产消费模型中消费者会取走数据,而读者写者模型中读者不会取走数据;

  • 读锁的优先级高

2.读写锁接口

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

  • 读者加锁:
    在这里插入图片描述

  • 写者加锁:

在这里插入图片描述
生产消费模型中,生产者和消费者的地位是对等的,这样才能达到最高效的状态
而读写者模型中,写者只有在读者全部退出的时候才能写,是读者优先的,这样就会发生写者饥饿问题;
读者写者问题中读锁的优先级高,是因为这种模型的应用场景为:数据的读取频率非常高,而被修改的频率特别低,这样有助于提升效率;

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Mac移动硬盘怎么识别PC电脑?

如果你拥有一台Mac设备&#xff0c;总会遇到尴尬的那一刻——你在Mac上用得好好的移动硬盘怎么都不能被PC识别到。又或者你朋友在PC上用得好好的移动硬盘&#xff0c;连上你的Mac后&#xff0c;Mac里的文件死活就是拷贝不进移动硬盘里。这种坑&#xff0c;相信大多数使用Mac的小…

webpack(一)模块化

模块化演变过程 阶段一&#xff1a;基于文件的划分模块方式 概念&#xff1a;将每个功能和相关数据状态分别放在单独的文件里 约定每一个文件就是一个单独的模块&#xff0c;使用每个模块&#xff0c;直接调用这个模块的成员 缺点&#xff1a;所有的成员都可以在模块外被访问和…

Kubernetes(K8s)基本环境部署

此处只做学习使用&#xff0c;配置单master环境。 一、环境准备 1、ip主机规划&#xff08;准备五台新机&#xff09;>修改各个节点的主机名 注意&#xff1a;关闭防火墙与selinux 节点主机名ip身份joshua1 kubernetes-master.openlab.cn 192.168.134.151masterjoshua2k…

mysql与msql2数据驱动

mysql基本使用 数据库操作&#xff08;DDL&#xff09; -- 数据考操作 -- 1.查询所有数据库 SHOW DATABASES;-- 2.选择数据库 USE learn_mysql;-- 3.当前正在使用的数据库 SELECT DATABASE();-- 4.创建数据库 CREATE DATABASE IF NOT EXISTS learn_mysql;-- 5.删除数据库 DRO…

亚马逊,eBay,速卖通买家账号是如何实现高权重,高存活率的

现在测评&#xff0c;补单机构越来越多&#xff0c;看似寻常的便捷渠道也潜藏着很大的风险&#xff0c;尤其是当大量机器代替人工、各种质量参差不齐的测评机构被曝光&#xff0c;跨境卖家“踩坑遇骗”的情况也就屡屡出现。很多卖家都选择自己注册买家账号&#xff0c;自己做测…

【uniapp】 实现公共弹窗的封装以及调用

图例&#xff1a;红框区域为 “ 内容区域 ” 一、组件 <!-- 弹窗组件 --> <template> <view class"add_popup" v-if"person.isShowPopup"><view class"popup_cont" :style"{width:props.width&&props.width&…

面向对象的设计原则

设计模式 Python 设计模式&#xff1a;对软件设计中普遍存在&#xff08;反复出现&#xff09;的各种问题&#xff0c;所提出的解决方案。每一个设计模式系统地命名、解释和评价了面向对象系统中一个重要的和重复出现的设计 面向对象 三大特性&#xff1a;封装、继承、多态 …

电商平台api对接货源

如今&#xff0c;电商平台已经成为了人们购物的主要途径之一。 然而&#xff0c;对于电商平台来说&#xff0c;货源对接一直是一个比较棘手的问题。为了解决这个问题&#xff0c;越来越多的电商平台开始使用API来对接货源。 API&#xff0c;即应用程序接口&#xff0c;是一种允…

c++入门一

参考&#xff1a;https://www.learncpp.com/cpp-tutorial/ When you finish, you will not only know how to program in C, you will know how NOT to program in C, which is arguably as important. Tired or unhappy programmers make mistakes, and debugging code tends…

5G智能网关如何解决城市停车痛点难点

2023年上半年&#xff0c;我国汽车新注册登记1175万辆&#xff0c;同比增长5.8%&#xff0c;88个城市汽车保有量超过100万辆&#xff0c;北京、成都等24个城市超过300万辆。随着车辆保有量持续增加&#xff0c;停车难问题长期困扰城市居民&#xff0c;也导致城市路段违停普遍、…

无涯教程-JavaScript - POISSON函数

POISSON函数取代了Excel 2010中的POISSON.DIST函数。 描述 该函数返回泊松分布。泊松分布的常见应用是预测特定时间的事件数。 语法 POISSON(x,mean,cumulative)争论 Argument描述Required/OptionalXThe number of events.RequiredMeanThe expected numeric value.Require…

积跬步至千里 || 数学基础、算法与编程

数学基础、算法与编程 1. BAP 技能 BAP 技能是指基础(Basic)、算法(Algorithm)和编程(Programm)三种基本技能的深度融合。理工科以数学、算法与编程为根基&#xff0c;这三个相辅相成又各有区别。 &#xff08;1&#xff09;数学以线性代数为主要研究工具和部分微积分技术为手…

Unity RenderStreaming 云渲染-黑屏

&#x1f96a;云渲染-黑屏 网页加载出来了&#xff0c;点击播放黑屏 &#xff0c;关闭防火墙即可&#xff01;&#xff01;&#xff01;&#xff01;

HarmonyOS/OpenHarmony(Stage模型)应用开发单一手势(二)

三、拖动手势&#xff08;PanGesture&#xff09; .PanGestureOptions(value?:{ fingers?:number; direction?:PanDirection; distance?:number}) 拖动手势用于触发拖动手势事件&#xff0c;滑动达到最小滑动距离&#xff08;默认值为5vp&#xff09;时拖动手势识别成功&am…

c#多线程—基础概念到“双色球”项目实现(附知识点目录、代码、视频)

总结&#xff1a;视频中对于多线程讲的非常透彻&#xff0c;从线程基础概念—>.net不同版本出现的线程方法—>多线程常出现问题—>双色球项目实践&#xff0c;每个知识点都有代码实操&#xff0c;受益匪浅。附上学习笔记和实操代码。 视频 目录 一、线程、进程概念及优…

HarmonyOS—使用Web组件加载页面

页面加载是 Web 组件的基本功能。根据页面加载数据来源可以分为三种常用场景&#xff0c;包括加载网络页面、加载本地页面、加载 HTML 格式的富文本数据。 页面加载过程中&#xff0c;若涉及网络资源获取&#xff0c;需要配置ohos.permission.INTERNET网络访问权限。 加载网络…

QEMU 仿真RISC-V freeRTOS 程序

1. 安裝RISC-V 仿真環境 --QEMU 安裝包下載地址: https://www.qemu.org/ 安裝命令及安裝成功效果如下所示, target-list 設定爲riscv32-softmmu, $ cat ~/project/qemu-8.0.4/install.sh sudo apt-get install libglib2.0-dev sudo apt-get install libpixman-1-dev ./co…

【MySQL】基础语法总结

MySQL 基础语句 一、DDL 数据库定义语言 1.1CREATE 创建 1.1.1 创建数据库 语法结构 CREATE DATABASE database_name;示例 CREATE DATABASE demo;1.1.2 创建表 语法结构 CREATE TABLE 表名 (列1 数据类型,列2 数据类型,... );示例 CREATE TABLE new_user (id INT PRIMARY KE…

软考A计划-网络工程师-复习背熟-数据通信基础和局域网技术

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列 &#x1f449;关于作者 专注于Android/Unity和各种游…

2.3 数据模型

思维导图&#xff1a; 前言&#xff1a; 我的理解&#xff1a; 这段话介绍了概念模型和数据模型之间的关系&#xff0c;以及数据模型的定义和重要性。具体解读如下&#xff1a; 1. **概念模型**&#xff1a;它是一种描述现实世界数据关系的抽象模型&#xff0c;不依赖于任何…