Linux《线程同步和互斥(下)》

在之前的Linux《线程同步和互斥(上)》当中我们已经了解了线程同步和互斥的基本概念,并且害学习了在pthread库当中提供的实现线程同步和互斥的接口,并且还试着封装了这些接口。除此之外我们还了解到了生产消费模型。那么在本篇当中我们将基于之前实现的类来实现一个线程池的设计,并且还会了解到一种未单例的设计模式,了解线程安全相关的概念。通过本篇的学习会让我们对线程有更深刻的理解,接下来久开始本篇的学习吧!!!



1.线程池

在此我们要实现一个线程池,用户可以将对应的任务插入到线程池,之后线程池就会调度对应的线程来执行对应的任务,用户无需关心具体的调度是什么样的,只需要将任务传给线程池即可。在此在实现线程池需要我们将对应的线程接口进行封装,条件变量和锁进行封装,还需要实现对应的日志。以上的封装的要求在之前的线程学习当中我已经实现了,但是还有日志的功能为实现,那么接下来就先来实现日志。

1.1 日志和策略模式

IT行业这么火, 涌入的⼈很多. 俗话说林子大了啥鸟都有。大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是⼤佬们针对⼀些经典的常见的场景, 给定了⼀些对应的解决方案, 这个就是 设计模式

在之前的学习当中我们了解到的生产者消费者模式、责任链模式、建造者模式等都是设计模式当中的一种,在接下来日志的学习当中我将了解到一种新的设计模式------策略模式。

在此在设计日志之前先来了解一下日志相关的概念以及日志的作用:

在计算机当中日志是进行系统和软件记录的重要文件,主要作用就是进行监控运行的状态、记录异常的信息,从而让程序员能更容易地定位到出现地地方,它是进行系统维护、故障排查地重要工具。
一般在日志当中需要包含以下的信息:

1.时间戳
2.日志等级
3.日志内容

除了以上的信息之外还可以包含以下的内容:
文件的行号 进程或者线程ID等信息

实际上日志是有现成的解决方法的,但是面前是我们第一次接触日志,那么这时采取的是自定义日志的方案。

在此我们要实现的日志形式如下所示:

cpp 复制代码
[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,⽀持可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world

以上在了解了日志相关的概念之后接下来就试着来实现我们自己的日志代码,一般来说日志的形成之后都是要生成到对应的文件当中,但是当前在我们的程序当中一般默认是将日志输出到显示器上,只有当用户指定输出到对应的文件的时候才会将日志进行文件的保存,实际上这种将执行的方式提供给用户进行选择的就是策略模式

策略模式本质上就是:提供多种可选的执行方式(策略),用户在运行时选择其中一种,程序就按对应的策略去执行。

那么接下来就要思考如何实现以上的要求,实际上我们只需要实现一个日志储存的基类,之后再生成不同日志存储的派生类即可。

在此文件的操作我们可以使用原来系统当中提供的系统调用,但是在此我们使用到了C++17当中了文件以及目录的操作,使用起来更简便一些。

cpp 复制代码
// 每个日志之间的分隔符
    const std::string gsep = "\r\n";
    // 日志基类
    class LogStrategy
    {
    public:
        LogStrategy() = default;
        // 日志储存到指定的文件当中
        virtual void Synclog(const std::string &message) = 0;
    };

    // 将日志储存到显示器文件当中
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy()
        {
        }
        ~ConsoleLogStrategy()
        {
        }
        void Synclog(const std::string &message) override
        {
            // 避免出现不一致问题对当前的写入操作进行加锁
            LockGuard lock(_mutex);
            std::cout << message << gsep;
        }

    private:
        Mutex _mutex;
    };

    // 写入到文件当中默认写入的文件
    const std::string defaultfile = "my.log";
    const std::string defaultpath = "./log";

    class FileLogstrategy : public LogStrategy
    {
    public:
        FileLogstrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
            : _path(path),
              _file(file)
        {
            // 多当前的操作进行加锁
            LockGuard lock(_mutex);
            // 判断当前的目录是否存在
            if (std::filesystem::exists(_path))
            {
                return;
            }
            // 不存在的时候就创建对应的目录
            try
            {
                std::filesystem::create_directories(_path);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << "\n";
            }
        }

        ~FileLogstrategy()
        {
        }

        // 进行日志的写入
        void Synclog(const std::string &message) override
        {
            LockGuard lock(_mutex);
            // 将文件的路径调整为./log/my.log的形式
            std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
            // 以追加的方式打开对应的日志文件
            std::ofstream f(filename, std::ios::app);
            if (!f.is_open())
            {
                return;
            }
            // 将日志的写入到文件当中
            f << message << "gsep";
            f.close();
        }

    private:
        // 日志文件名
        std::string _file;
        // 日志文件路径
        std::string _path;
        // 锁
        Mutex _mutex;
    };

实现了以上日志的储存代码之后接下来就需要来实现日志具体的实现,首先来实现的就是日志等级的实现,在此使用枚举类型来实现,但是在输出的时候我们是要输出字符串,因此就需要再实现将对应的枚举类型转换为字符串的函数。

cpp 复制代码
  //日志等级
    enum class LogLevel
    {
        DEBUG,//调试
        INFO,//信息
        WARNING,//警告
        ERROR,//错误
        FATAL//致命错误

    };

    //将对应的日志等级转换为对应的字符串
    std::string Leveltostr(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        default:
            return "NONE";
        }
    }

接下来再来实现日志当中的当前的时间,我知道使用time是可以获得到当前的时间戳的,但是面前的问题是我们是需要将在日志输出 [年-月-日 时:分:秒]的格式,那么这就需要我们进行时间戳到时间的转换,实际上在系统当中提供了时间戳到格式化时间的转换函数,如下所示:

cpp 复制代码
#include <time.h>


struct tm *localtime_r(const time_t *timep, struct tm *result);

参数:
timep:要进行装换的时间戳
result:转换之后保存的tm结构体

struct tm内成员变量如下所示:
struct tm
{
  int tm_sec;			/* Seconds.	[0-60] (1 leap second) */
  int tm_min;			/* Minutes.	[0-59] */
  int tm_hour;			/* Hours.	[0-23] */
  int tm_mday;			/* Day.		[1-31] */
  int tm_mon;			/* Month.	[0-11] */
  int tm_year;			/* Year	- 1900.  */
  int tm_wday;			/* Day of week.	[0-6] */
  int tm_yday;			/* Days in year.[0-365]	*/
  int tm_isdst;			/* DST.		[-1/0/1]*/
//......
};

有了以上的函数之后接下来就可以将时间戳装换为对应的tm结构体,之后再根据结构体当中的变量装换的为指定格式的字符串,集体实现的代码如下所示:

cpp 复制代码
    //获取当前的时间戳,并且装换为指定的格式 2025-10-04 14:38:16
    std::string GetTime()
    {
        time_t cur = time(nullptr);
        //格式化时间戳的结构体tm
        struct tm cur_tm;
        //进行转换
        localtime_r(&cur, &cur_tm);
        char timebuffer[128];
        //将时间格式化输出
        snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
                 cur_tm.tm_year + 1900,
                 cur_tm.tm_mon + 1,
                 cur_tm.tm_mday,
                 cur_tm.tm_hour,
                 cur_tm.tm_min,
                 cur_tm.tm_sec);
        return timebuffer;
    }

实现了以上日志实现的前提工作之后接下来就可以试着来实现日志类的,实现的代码如下所示:

cpp 复制代码
    // 日志类
    class Logger
    {
    public:
        // 默认是将日志写入到显示器文件当中
        Logger()
        {
            EnableConsoleLogStategy();
        }

        // 将日志写入到指定的文件当中
        void EnableFileLogStategy()
        {
            _fflush_strategy = std::make_unique<FileLogstrategy>();
        }

        // 将日志写入到显示器文件当中
        void EnableConsoleLogStategy()
        {
            _fflush_strategy = std::make_unique<ConsoleLogStrategy>();
        }

        // 日志消息类
        class LogMessage
        {

        public:
            // 日志消息构造函数,将[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] -写入到------loginfo字符串当中
            LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
                : _curr_time(GetTime()),
                  _level(level),
                  _pid(getpid()),
                  _src_name(src_name),
                  _line_number(line_number),
                  _logger(logger)
            {
                std::stringstream ss;
                ss << "[" << _curr_time << "]"
                   << "[" << Leveltostr(_level) << "]"
                   << "[" << _pid << "]"
                   << "[" << _src_name << "]"
                   << "[" << _line_number << "]"
                   << "- ";
                _loginfo = ss.str();
            }

            // 日志消息当中的<<运算符重载函数
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                // 将info字符串内的内容追加到_loginfo内
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this;
            }

            // 当Logmessage析构的时候将_loginfo写入到对应的文件当中
            ~LogMessage()
            {
                if (_logger._fflush_strategy)
                {
                    _logger._fflush_strategy->Synclog(_loginfo);
                }
            }

        private:
            // 日志发生时间
            std::string _curr_time;
            // 日志等级
            LogLevel _level;
            // 进程pid
            pid_t _pid;
            // 发生所处的文件名
            std::string _src_name;
            // 文件的行号
            int _line_number;

            // 保存日志的字符串
            std::string _loginfo;
            Logger &_logger;
        };

        // 日志类当中的仿函数
        LogMessage operator()(LogLevel level, std::string name, int line)
        {
            // 将日志的类型、发生的文件名、文件的行号等传给文件消息对象
            return LogMessage(level, name, line, *this);
        }

    private:
        // 日志策略
        std::unique_ptr<LogStrategy> _fflush_strategy;
    };

实际上以上实现日志基本的思路就是首先先创建出一个Log的对象,用户可以通过对应的函数调用来选择日志输出的目的地,之后用户通过仿函数来向日志传入对应的日志等级等属性,传入之后构造出对应的LogMessage对象,接下来就通过用户使用<<将日志的内容输入,最后将格式化之后的字符串通过调用Synclog来实现日志最后的写入。

以上我们就实现的对应的日志代码,接下来就进行测试看实现的是否满足我们的要求,代码如下所示:

cpp 复制代码
#include<iostream>
#include"log.hpp"

using namespace LogModule;

int main()
{
    Logger log;

    log.EnableConsoleLogStategy();
    log(LogLevel::DEBUG,"test.cc",11)<<"日志测试1";
    log(LogLevel::WARNING,"test.cc",12)<<"日志测试2";


    return 0;
}

编译以上的程序输出的结果如下所示:

通过输出的结果可以看出是符合我们的要求的,但是面前问题是目前的日志使用起来还是较为繁琐,需要我们先创建出对应的Loggerd对象之后在输出日志的时候还需要传当前代码文件的文件名和行号,这样使用起来还是稍显麻烦,那么在log.hpp当中就使用一些宏来让日志的使用更为简便。

cpp 复制代码
Logger logger;

#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_log_Strategy() logger.EnableConsoleLogStategy()
#define Enable_File_log_Strategy() logger.EnableFileLogStategy()

有了以上的之后接下来就可以按照一下的方式进行日志的输出:

cpp 复制代码
#include<iostream>
#include"log.hpp"

using namespace LogModule;

int main()
{

    Enable_Console_log_Strategy();

    LOG(LogLevel::DEBUG)<<"日志测试1";
    LOG(LogLevel::WARNING)<<"日志测试2";


    return 0;
}

日志实现完整代码

实现的日志的代码如下所示:

cpp 复制代码
#pragma once

#include <iostream>
#include <fstream>
#include <filesystem>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sstream>
#include <memory>
#include "Mutex.hpp"

using namespace MutexNamespace;

namespace LogModule
{
    // 每个日志之间的分隔符
    const std::string gsep = "\r\n";
    // 日志基类
    class LogStrategy
    {
    public:
        LogStrategy() = default;
        // 日志储存到指定的文件当中
        virtual void Synclog(const std::string &message) = 0;
    };

    // 将日志储存到显示器文件当中
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy()
        {
        }
        ~ConsoleLogStrategy()
        {
        }
        void Synclog(const std::string &message) override
        {
            // 避免出现不一致问题对当前的写入操作进行加锁
            LockGuard lock(_mutex);
            std::cout << message << gsep;
        }

    private:
        Mutex _mutex;
    };

    // 写入到文件当中默认写入的文件
    const std::string defaultfile = "my.log";
    const std::string defaultpath = "./log";

    class FileLogstrategy : public LogStrategy
    {
    public:
        FileLogstrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
            : _path(path),
              _file(file)
        {
            // 多当前的操作进行加锁
            LockGuard lock(_mutex);
            // 判断当前的目录是否存在
            if (std::filesystem::exists(_path))
            {
                return;
            }
            // 不存在的时候就创建对应的目录
            try
            {
                std::filesystem::create_directories(_path);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << "\n";
            }
        }

        ~FileLogstrategy()
        {
        }

        // 进行日志的写入
        void Synclog(const std::string &message) override
        {
            LockGuard lock(_mutex);
            // 将文件的路径调整为./log/my.log的形式
            std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
            // 以追加的方式打开对应的日志文件
            std::ofstream f(filename, std::ios::app);
            if (!f.is_open())
            {
                return;
            }
            // 将日志的写入到文件当中
            f << message << "gsep";
            f.close();
        }

    private:
        // 日志文件名
        std::string _file;
        // 日志文件路径
        std::string _path;
        // 锁
        Mutex _mutex;
    };

    // 日志等级
    enum class LogLevel
    {
        DEBUG,   // 调试
        INFO,    // 信息
        WARNING, // 警告
        ERROR,   // 错误
        FATAL    // 致命错误

    };

    // 将对应的日志等级转换为对应的字符串
    std::string Leveltostr(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        default:
            return "NONE";
        }
    }

    // 获取当前的时间戳,并且装换为指定的格式 2025-10-04 14:38:16
    std::string GetTime()
    {
        time_t cur = time(nullptr);
        // 格式化时间戳的结构体tm
        struct tm cur_tm;
        // 进行转换
        localtime_r(&cur, &cur_tm);
        char timebuffer[128];
        // 将时间格式化输出
        snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
                 cur_tm.tm_year + 1900,
                 cur_tm.tm_mon + 1,
                 cur_tm.tm_mday,
                 cur_tm.tm_hour,
                 cur_tm.tm_min,
                 cur_tm.tm_sec);
        return timebuffer;
    }

    // 日志类
    class Logger
    {
    public:
        // 默认是将日志写入到显示器文件当中
        Logger()
        {
            EnableConsoleLogStategy();
        }

        // 将日志写入到指定的文件当中
        void EnableFileLogStategy()
        {
            _fflush_strategy = std::make_unique<FileLogstrategy>();
        }

        // 将日志写入到显示器文件当中
        void EnableConsoleLogStategy()
        {
            _fflush_strategy = std::make_unique<ConsoleLogStrategy>();
        }

        // 日志消息类
        class LogMessage
        {

        public:
            // 日志消息构造函数,将[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] -写入到------loginfo字符串当中
            LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
                : _curr_time(GetTime()),
                  _level(level),
                  _pid(getpid()),
                  _src_name(src_name),
                  _line_number(line_number),
                  _logger(logger)
            {
                std::stringstream ss;
                ss << "[" << _curr_time << "]"
                   << "[" << Leveltostr(_level) << "]"
                   << "[" << _pid << "]"
                   << "[" << _src_name << "]"
                   << "[" << _line_number << "]"
                   << "- ";
                _loginfo = ss.str();
            }

            // 日志消息当中的<<运算符重载函数
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                // 将info字符串内的内容追加到_loginfo内
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this;
            }

            // 当Logmessage析构的时候将_loginfo写入到对应的文件当中
            ~LogMessage()
            {
                if (_logger._fflush_strategy)
                {
                    _logger._fflush_strategy->Synclog(_loginfo);
                }
            }

        private:
            // 日志发生时间
            std::string _curr_time;
            // 日志等级
            LogLevel _level;
            // 进程pid
            pid_t _pid;
            // 发生所处的文件名
            std::string _src_name;
            // 文件的行号
            int _line_number;

            // 保存日志的字符串
            std::string _loginfo;
            Logger &_logger;
        };

        // 日志类当中的仿函数
        LogMessage operator()(LogLevel level, std::string name, int line)
        {
            // 将日志的类型、发生的文件名、文件的行号等传给文件消息对象
            return LogMessage(level, name, line, *this);
        }

    private:
        // 日志策略
        std::unique_ptr<LogStrategy> _fflush_strategy;
    };

    Logger logger;

#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_log_Strategy() logger.EnableConsoleLogStategy()
#define Enable_File_log_Strategy() logger.EnableFileLogStategy()

}

1.2 线程池实现

以上我们实现了日志之后就可以将线程池之前的准备工作都完成了,那么接下来就可以来实现线程池了,但是在实现之前还是先来了解一下线程池具体的概念。

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

线程池适用的场景如下所示:
1.需要大量的线程来完成任务,且完成任务的时间比较短。 比如WEB服务器完成网页请求这样的任务,使⽤线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象⼀个热门⽹站的点击次数。 但对于长时间的任务,比如⼀个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间⽐线程的创建时间⼤多了。

2.对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。

3.接受突发性的大量请求,但不⾄于使服务器因此产生大量线程的应⽤。突发性大量客户请求,在没有线程池情况下,将产⽣大量线程,虽然理论上大部分操作系统线程数⽬最⼤值不是问题,短时间内产⽣大量线程可能使内存到达极限,出现错误。

线程池的种类

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

b. 浮动线程池,除了线程吃当中的线程个数不是固定的其他和固定线程池一样。

实际上线程池也是一个生产者消费者模型,线程池当中的任务队列就是交易场所,各个线程就是消费者,将任务插入到队列当中的用户就是消费者。

了解了线程池的相关的概念之后接下来就可以来就可以来实现线程池的代码:

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include "log.hpp"
#include "cond.hpp"
#include "Mutex.hpp"
#include "Thread.hpp"

namespace ThreadPoolModule
{
    //引入之前实现的封装的互斥锁、条件变量、线程等
    using namespace CondNamespace;
    using namespace MutexNamespace;
    using namespace ThreadModlue;
    using namespace LogModule;

    //线程池当中默认的线程个数
    static const int gnum = 5;


    //线程池类
    template <class T>
    class ThreadPool
    {

    private:
        //唤醒所有的线程
        void WakeUpAllThread()
        {
            LockGuard lock(_mutex);
            if (_sleepernum)
                _cond.Broadcast();
            LOG(LogLevel::INFO) << "唤醒所有休眠线程";
        }


        //唤醒一个线程
        void WakeUpoOne()
        {
            _cond.Signal();
            LOG(LogLevel::INFO) << "唤醒一个休眠线程";
        }
    public:

        //线程池构造函数
        ThreadPool(int num = gnum)
            : _num(num),
              _isrunning(false),
              _sleepernum(0)
        {
            for (int i = 0; i < num; i++)
            {
                _threads.emplace_back(
                    [this]()
                    {
                        HandlerTask();
                    });
            }
        }

        //启动线程池
        void Start()
        {
            if (_isrunning)
                return;
            _isrunning = true;
            for (auto &x : _threads)
            {
                x.Start();
                LOG(LogLevel::INFO) << "start new thread success";
            }
        }

      
        //暂停线程池
        void Stop()
        {
            if (!_isrunning)
                return;
            _isrunning = false;
            //唤醒所有线程
            WakeUpAllThread();
        }

        //进行线程等待
        void Join()
        {
            for (auto &thrad : _threads)
            {
                thrad.Join();
            }
        }

        //线程执行任务
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T t;

                {
                    LockGuard lock(_mutex);
                    //当前任务队列为空并且线程还在运行状态
                    while (_taskq.empty() && _isrunning)
                    {
                        _sleepernum++;
                        //进行将当前线程进行休眠
                        _cond.Wait(_mutex);
                        _sleepernum--;
                    }
                    //当前线程池不在运行状态并且任务队列为空
                    if (!_isrunning && _taskq.empty())
                    {
                        //让当前的线程结束运行
                        LOG(LogLevel::INFO) << name << "退出了,线程池退出或者任务队列为空";
                        break;
                    }

                    //能走到这里表明任务队列当中一定有剩余没有取出的任务
                    //从任务队列当中当中获取任务
                    t = _taskq.front();
                    _taskq.pop();
                }
                //执行任务,在执行任务过程中线程之间不存在互斥的关系,不需要进行加锁
                t();
            }
        }

        //向线程池当中插入任务
        bool Enqueue(const T &in)
        {
            if (_isrunning)
            {
                //将任务添加到任务队列当中
                LockGuard lock(_mutex);
                _taskq.push(in);
                //当前存在休眠的线程,就唤醒一个线程
                if ( _sleepernum>0)
                {
                    WakeUpoOne();
                }
                return true;
            }
            return false;
        }

        ~ThreadPool()
        {
        }


    private:
        //存储线程的数组
        std::vector<Thread> _threads;
        //线程池当中的线程个数
        int _num;
        //任务队列
        std::queue<T> _taskq;
        //条件变量
        Cond _cond;
        //互斥锁
        Mutex _mutex;

        //当前线程池是否运行标志位
        bool _isrunning;
        //线程池当中处于休眠的线程个数
        int _sleepernum;
    };


}

以上我们实现出了线程池对应的代码,那么接下来就使用以上的线程池来进行一些任务的处理,在此创建一个Task.hpp的类,在该类当中实现对应的任务:

cpp 复制代码
#pragma once 
#include"log.hpp"
#include<functional>

using namespace LogModule;


using func_t =std::function<void()>;

void Download()
{

    LOG(LogLevel::DEBUG)<<"下载任务正在进行";
}

在mian.cc当中向线程池当中插入对应的Download任务。

cpp 复制代码
#include"ThreadPool.hpp"
#include"Task.hpp"


using namespace ThreadPoolModule;

int main()
{

     ThreadPool<func_t>* t=new ThreadPool<func_t>();

     t->Start();
    int count=10;
    while(count)
    {
        t->Enqueue(Download);
        sleep(1);
        count--;
    }
    sleep(1);

    t->Stop();
    t->Join();


    return 0;
}

通过输出的结果就可以看出实现的线程池是没有问题的。

3.3 单例模式

某些类在实例化的过程当中只能有一个对象,那么就将这种模式称为单例模式。

那么这时候就要思考为什么在实例化的过程当中只能有一个对象?
其实一些数据的内容是非常多的,创建一次对应的对象就需要大量的内存以及时间,这时候将实例化只能进行一次就更加的合理,同时在此使用单例模式能让系统的接口更加的规范,保证只能有一个接口进行访问。

实际上在单例模式当中是可以划分为两种模式的,分别是懒汉模式饿汉模式

以下是两个模式具体的区别:

懒汉模式(Lazy Singleton)
特点
:实例在第一次使用时才创建。
优点 :节省内存资源,如果程序运行过程中根本没有用到该对象,就不会占用空间。
缺点 :实现上要考虑 线程安全,否则在多线程环境可能会出现多个实例。

就像 点外卖 。你饿了才点,外卖才开始做。
好处:不浪费,如果你根本没饿,就不用做饭。
坏处:等饭的时间可能比较久。

饿汉模式(Eager Singleton)
特点
:实例在程序启动时就已经创建好。
优点 :实现简单,天然线程安全 (因为类加载时就创建完成)。
缺点:即使程序运行过程中没用到这个对象,也会一直占用内存。

就像 提前做好便当 。你不管今天饿不饿,早上就把饭做好了。
好处:想吃的时候马上有。
坏处:可能浪费(如果你不吃,这顿饭就浪费了)。

在代码当中懒汉模式和饿汉模式的具体的区别就是:在懒汉当中是会一开始加载的时候就将对应的对象实例化,懒汉模式当中一开始在加载的时候未进行实例化而是等到第一次使用的时候再进行实例化。

例如以下的示例:

cpp 复制代码
//饿汉模式实现单例
template <typename T>
class Singleton 
{
    static T data;
public:
    static T* GetInstance() 
    {
        return &data;
    }
};

//懒汉模式实现单例
template <typename T>
class Singleton 
{
    static T* inst;
public:
    static T* GetInstance() 
    {
        if (inst == NULL) 
        {
            inst = new T();
        } 
    return inst;
    }
};

但是以上懒汉模式实现实际上还是存在问题的,那么就是当前可能会存在两个线程同时进入到临界区当中,那么这时候久会出现创建出两个对象的问题。那么这时候就需要使用锁来避免以上的问题。

cpp 复制代码
// 懒汉模式, 线程安全
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;
    }
}

以上就了解了单例模式的概念,那么接下来就可以试着将以上外卖实现的线程池修改为单例模式。

首先我们需要做的是将构造函数私有化,接下来在实现一个静态成员函数,让用户能得到对应对象的指针,并且还需要实现一个静态成员的锁来保证用户在获取的过程当中是互斥的。

cpp 复制代码
    template <class T>
    class ThreadPool
    {

       //......

    public:
        static ThreadPool<T> *GwetInstance()
        {
            LOG(LogLevel::INFO) << "获取单例";
            if (_pthread == nullptr)
            {
                LockGuard lock(_lock);
                if (_pthread == nullptr)
                {
                    LOG(LogLevel::INFO) << "创建一个单例";
                    _pthread = new ThreadPool<T>();
                    _pthread->Start();
                }
            }

            return _pthread;
        }

    private:
       //......

        static ThreadPool<T> *_pthread;
        static Mutex _lock;
    };

    template <class T>
    ThreadPool<T> *ThreadPool<T>::_pthread = nullptr;

    template <class T>
    Mutex ThreadPool<T>::_lock;

2. 线程安全和重入问题

以下是两个的概念:

线程安全:就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。⼀般而言,多个线程并发同⼀段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进⾏操作,并且没有锁保护的情况下,容易出现该问题。

重入:同⼀个函数被不同的执行流调用,当前⼀个流程还没有执行完,就有其他的执⾏流再次进入,我们称之为重入。⼀个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重⼊函数,否则,是不可重入函数。

到目前为止,我们学习的重入实际上分为以下的两种:
1.多线程重入函数
2.信号导致一个执行流重复进入函数

实际上对应线程安全和重入只需要理解以下的即可:

📌可重入与线程安全联系

函数是可重入的,那就是线程安全的(其实知道这⼀句话就够了)

• 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

• 如果⼀个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
📌 可重入与线程安全区别

可重入函数是线程安全函数的⼀种

线程安全不⼀定是可重入的,而可重入函数则⼀定是线程安全的。

如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

注意:

• 如果不考虑 信号导致⼀个执行流重复进⼊函数 这种重入情况,线程安全和重入在安全角度不做区分
• 但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点
• 可重入描述的是⼀个函数是否能被重复进入,表示的是函数的特点

3. 死锁

死锁是指在一组的线程当中各个进程均占有不会释放的资源,但互相申请其他进程所占用不会释放的资源而处于的一种永久的等待状态。

例如以下的示例,当前有线程A和线程B,线程必须同时的拥有锁1和锁2,才能进行之后资源的访问

在此申请一把锁的时候是原子的,但是两把就不是了

这时就会造成以下的结果:

产生死锁的四个必要条件

• 互斥条件:⼀个资源每次只能被⼀个执行流使用

• 请求与保持条件:⼀个执行流因请求资源而阻塞时,对已获得的资源保持不放

• 不剥夺条件:⼀个执行流已获得的资源,在末使用完之前,不能强行剥夺

• 循环等待条件:若干执行流之间形成⼀种头尾相接的循环等待资源的关系

4. STL,智能制造和线程安全

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

其实不是的。原因是, STL 的设计初衷是将性能挖掘到极致, 而⼀旦涉及到加锁保证线程安全, 会对性能造成巨大的影响。

而且对于不同的容器, 加锁⽅式的不同, 性能可能也不同(例如hash表的锁表和锁桶).

因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.

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

对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.

对于 shared_ptr, 多个对象需要共用⼀个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够搞效,原子的操作引用计数。

以上就是本篇的全部内容了,在此之后我们将开始Linux当中网络部分的学习,未完待续......

相关推荐
zhangrelay3 小时前
蓝桥云课中支持的ROS1版本有哪些?-2025在线尝试ROS1全家福最方便的打开模式-
linux·笔记·学习·ubuntu
2301_818411553 小时前
rpm软件包管理以及yum,apt的前端软件包管理器
linux·运维·服务器
CS Beginner4 小时前
【Linux】安装配置mysql中出现的问题1
linux·mysql·adb
aitav05 小时前
⚡ arm 32位嵌入式 Linux 系统移植 QT 程序
linux·arm开发·qt
骄傲的心别枯萎5 小时前
项目1:FFMPEG推流器讲解(二):FFMPEG输出模块初始化
linux·ffmpeg·音视频·视频编解码·rv1126
Nimsolax5 小时前
Linux网络应用层自定义协议与序列化
linux·网络
egoist20235 小时前
[linux仓库]图解System V共享内存:从shmget到内存映射的完整指南
linux·开发语言·共享内存·system v
葵花日记5 小时前
LINUX——进度条
linux·运维·服务器
hmcjn(小何同学)6 小时前
轻松Linux-10.进程信号
linux·运维·服务器