Linux便捷的系统之----线程池

1.线程池

0)总述

在前段时间我们写了进程池的代码,那么今天,我们已经学习完了线程的内容,下面我们也来写一下线程池吧~这是本Linux系统中最后一个大项目了,逻辑较为复杂,需要综合前几篇博客的代码!这里仅讲解分享新写的内容,旧的代码内容直接给出~

要设计线程池,必然涉及多线程,那锁是肯定需要了~,之后锁的开关要看条件变量,那之前的comnd.hpp文件也不能少~,之后我们要像是日志那样打印出来,上一篇博客的日志代码也需要了~我们设计的是线程池,是一个池化的工程,里面必然有很多线程吧?那就又要在写一个文件来实现各个线程~之后我们要线程去做任务,那么之前写的Task文件应该也需要了~

先把之前的代码放在这里:

1)mutex.hpp

#pragma once
#include 
#include 
#include 
class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    void UnLock()
    {
        pthread_mutex_unlock(&_lock);
    }
    pthread_mutex_t *Get()
    {
        return &_lock;
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};
class LockGuard
{
public:
    LockGuard(Mutex*_mutex)
    :_mutexp(_mutex)
    {
        _mutex->Lock();
    }
    ~LockGuard()
    {
        _mutexp->UnLock();
    }
private:
    Mutex* _mutexp;
};

2)Cond.hpp

#pragma once
#include
#include
#include"Mutex.hpp"
class Cond
{
public:
    Cond()
    {
        pthread_cond_init(&_cond,nullptr);
    }
    void Wait(Mutex &lock)
    {
        int n=pthread_cond_wait(&_cond,lock.Get());
    }
    void NotifyOne()   //激活一个线程
    {
        int n=pthread_cond_signal(&_cond);
        (void)n;
    }
    void NotifyAll()
    {
        int n=pthread_cond_broadcast(&_cond);
        (void)n;
    }
    ~Cond()
   {
    pthread_cond_destroy(&_cond);
   }
private:
    pthread_cond_t _cond;
};

3)Task.hpp

#pragma once
#include 
#include 
#include 
using namespace std;
class Task
{
public:
    Task()
    {
    }
    Task(int x, int y) : a(x), b(y)
    {
    }
    void Excute()
    {
        result=a+b;
    }
    void operator()()
    {
        Excute();
    }
    void Print()
    {
        cout<#pragma once
#include 
#include 
#include  // C++17 文件操作
#include 
#include 
#include 
#include 
#include 
#include "Mutex.hpp"
using namespace std;
// 规定出场景的日志等级
enum class LogLevel
{
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};
string Level2String(LogLevel level)
{
    switch (level)
    {
    case LogLevel::DEBUG:
        return "DEBUG";
    case LogLevel::INFO:
        return "Info";
    case LogLevel::WARNING:
        return "Warning";
    case LogLevel::ERROR:
        return "Error";
    case LogLevel::FATAL:
        return "Fatal";
    default:
        return "Unknown";
    }
}
// 20XX-08-04 12:27:03
string GetCurrentTime()
{
    // 1. 获取时间戳
    time_t currtime = time(nullptr);
    // 2. 如何把时间戳转换成为20XX-08-04 12:27:03
    struct tm currtm;
    localtime_r(&currtime, &currtm);
    // 3. 转换成为字符串 -- dubug?
    char timebuffer[64];
    snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
             currtm.tm_year + 1900,
             currtm.tm_mon + 1,
             currtm.tm_mday,
             currtm.tm_hour,
             currtm.tm_min,
             currtm.tm_sec);
    return timebuffer;
}
// 1. 刷新的问题 -- 假设我们已经有了一条完整的日志,string->设备(显示器,文件)
// 基类方法
class LogStrategy
{
public:
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const string &logmessage) = 0;
};
// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:
    ~ConsoleLogStrategy()
    {
    }
    void SyncLog(const string &logmessage) override
    {
        {
            LockGuard lockgurad(&_lock);
            cout << logmessage << endl;
        }
    }
private:
    Mutex _lock;
};
const std::string logdefaultdir = "log";
const static std::string logfilename = "test.log";
// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:
    FileLogStrategy(const string &dir = logdefaultdir,
                    const string filename = logfilename)
        : _dir_path_name(dir), _filename(filename)
    {
        LockGuard lockguard(&_lock);
        if (filesystem::exists(_dir_path_name))
        {
            return;
        }
        try
        {
            filesystem::create_directories(_dir_path_name);
        }
        catch (const filesystem::filesystem_error &e)
        {
            std::cerr << e.what() << "\r\n";
        }
    }
    void SyncLog(const string &logmessage) override
    {
        LockGuard lockguard(&_lock);
        string target = _dir_path_name;
        target += "/";
        target += _filename;
        ofstream out(target.c_str(), std::ios::app);
        if (!out.is_open())
        {
            return;
        }
        out << logmessage << "\n"; // out.write
        out.close();
    }
    ~FileLogStrategy()
    {
    }
private:
    string _dir_path_name; // log
    string _filename;      // hello.log => log/hello.log
    Mutex _lock;
};
// 网络刷新
// 1. 定制刷新策略
// 2. 构建完整的日志
class Logger
{
public:
    Logger()
    {
    }
    void EnableConsoleLogStrategy() // 显示器刷新
    {
        _strategy = make_unique();
    }
    void EnableFileLogStrategy()
    {
        _strategy = make_unique();
    }
    // 形成一条完整日志的方式
    class LogMessage
    {
    public:
        LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
            : _curr_time(GetCurrentTime()),
              _level(level),
              _pid(getpid()),
              _filename(filename),
              _line(line),
              _logger(logger)
            {
            std::stringstream ss;
            ss << "[" << _curr_time << "] "
               << "[" << Level2String(_level) << "] "
               << "[" << _pid << "] "
               << "[" << _filename << "] "
               << "[" << _line << "]"
               << " - ";
            _loginfo = ss.str();
            }
        template
        LogMessage& operator << (const T &info)
        {
            stringstream ss;
            ss<SyncLog(_loginfo);
            }
        }
    private:
        string _curr_time; // 日志时间
        LogLevel _level;   // 日志等级
        pid_t _pid;        // 进程pid
        string _filename;
        int _line;
        string _loginfo; // 一条合并完成的,完整的日志信息
        Logger &_logger; // 提供刷新策略的具体做法
    };
    LogMessage operator()(LogLevel level, std::string filename, int line)
    {
        return LogMessage(level, filename, line, *this);
    }
    ~Logger()
    {
    }
private:
    unique_ptr _strategy;
};
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()

下面是本节的重点代码:

首先就是线程的设计,我们还是可以用一个类来封装我们的线程,其私有成员变量包括

_isrunning:表示线程是否正在运行。

_tid:线程的POSIX线程ID。

_lwpid:线程的轻量级进程ID。

_name:线程的名称。

_func:线程函数,类型为 func_t

之后对其进行初始化,写一个构造函数即可,

线程函数上,包括start,stop,join,以及析构函数,这些就是一些C++库函数的调用与封装,这里直接给出代码:

5)Thread.hpp

#ifndef __THREAD_HPP__
#define __THREAD_HPP__
#include 
#include 
#include 
#include 
#include 
#include  /* For SYS_xxx definitions */
#include "Logger.hpp"
#define get_lwp_id() syscall(SYS_gettid)
using namespace std;
using func_t = function;
const string threadnamedefault = "None-Name";
class Thread
{
public:
    Thread(func_t func,const string& name=threadnamedefault)
    :_name(name)
    ,_func(func)
    ,_isrunning(false)
    {
        LOG(LogLevel::INFO)<<_name<<" create thread obj success";
    }
    static void *start_route(void *args)   //让线程开始运行
    {
        Thread *self=static_cast(args);
        self->_isrunning=true;
        self->_lwpid=get_lwp_id();
        self->_func(self->_name);
        pthread_exit((void*)0);
    }
    void Start()
    {
        int n=pthread_create(&_tid,nullptr,start_route,this);      //???为什么传递this指针
        if(n==0)
        {
            LOG(LogLevel::INFO)<<_name<<"running success";
        }
    }
    void Stop()
    {
        int n=pthread_cancel(_tid);// 太简单粗暴了
        (void)n;
    }
    // 检测线程结束并且回收的功能
    void Join()
    {
        if(!_isrunning)
            return;
        int n=pthread_join(_tid,nullptr);
        if(n==0)
        {
            LOG(LogLevel::INFO)<<_name<<" pthread_join success";
        }
    }
    ~Thread()
    {
    }
private:
    bool _isrunning;
    pthread_t _tid;
    pid_t _lwpid;
    string _name;
    func_t _func;
};
#endif

下面是线程池的设计:

首先还是用一个类来封装,其私有成员主要包含一下内容:任务队列、多个线程、保护机制、是否运行、队列判空、以及线程执行逻辑,类的成员函数包括入队列,开始运行,停止运行,以及等待,大体逻辑跟上述差不多,直接看代码吧,里面有注释

6)ThreadPool.hpp

#pragma once
#include 
#include 
#include 
#include 
#include 
#include "Mutex.hpp"
#include "Cond.hpp"
#include "thread.hpp"
// 普通线程池
const static int defaultthreadnum = 3; // for debug
template 
class ThreadPool
{
public:
    ThreadPool(int threadnum = defaultthreadnum)
        : _threadnum(threadnum), _is_running(false), _wait_thread_num(0)
    {
        for (int i = 0; i < _threadnum; i++)
        {
            // 方法1:
            // auto f = std::bind(hello, this);
            // 方法2
            string name = "thread-" + to_string(i + 1);
            _threads.emplace_back([this](const string &name)
                                  { this->Routine(name); }, name);
        }
        LOG(LogLevel::INFO) << "thread pool obj create success";
    }
    void Start()
    {
        if (_is_running) // 线程要是已经跑起来了那就直接返回好了,这个函数对其来说就没啥用了
            return;
        _is_running = true;
        for (auto &t : _threads)
        {
            t.Start();
        }
        LOG(LogLevel::INFO) << "thread pool running success";
    }
    // 核心思想:我们应该让线程走正常的唤醒逻辑退出
    // 线程池要退出
    // 1. 如果被唤醒 && 任务队列没有任务 = 让线程退出
    // 2. 如果被唤醒 && 任务队列有任务 = 线程不能立即退出,而应该让线程把任务处理完,在退出
    // 3. 线程本身没有被休眠,我们应该让他把他能处理的任务全部处理完成, 在退出
    // 3 || 2 -> 1
    // 如果任务队列有任务,线程是不会休眠的!
    void Stop()
    {
        if (!_is_running) // 已经不运行了
            return;
        _is_running = false;
        if (_wait_thread_num) // 还有等待的线程
            _cond.NotifyAll();
        // 这种做法不推荐
        // if (!_is_running)
        //     return;
        // _is_running = false;
        // for (auto &t : _threads)
        // {
        //     t.Stop();
        // }
        // LOG(LogLevel::INFO) << "thread pool stop success";
    }
    void Wait()
    {
        for (auto &t : _threads)
        {
            t.Join();
        }
        LOG(LogLevel::INFO) << "thread pool wait success";
    }
    void Enqueue(const T &t)
    {
        if (!_is_running) // 线程都不运行了,那就别提入队列了~
            return;
        {
            LockGuard lockguard(&_lock);
            _q.push(t);
            if (_wait_thread_num > 0)
                _cond.NotifyOne();
        }
    }
    ~ThreadPool()
    {
    }
private:
    // 任务队列
    std::queue _q; // 整体使用的临界资源
    // 多个线程
    vector _threads; // 1. 创建线程对象 2. 让线程对象启动
    int _threadnum;
    int _wait_thread_num;
    // 保护机制
    Mutex _lock;
    Cond _cond;
    // 其他属性
    bool _is_running;
    bool QueueIsEmpty()
    {
        return _q.empty();
    }
    void Routine(const string &name)
    {
        while (1)
        {
            // 把任务从线程获取到线程私有!临界区 -> 私有的栈
            T t;
            {
                LockGuard lockguard(&_lock);
                while (QueueIsEmpty() && _is_running) // 队列为空并且运行
                {
                    _wait_thread_num++;
                    _cond.Wait(_lock);
                    _wait_thread_num--;
                }
                if (!_is_running && QueueIsEmpty()) // 队列为空并且线程没运行
                {
                    LOG(LogLevel::INFO) << " 线程池退出 && 任务队列为空, " << name << " 退出";
                    break;
                }
                // 队列中一定有任务了!, 但是
                // 1. 线程池退出 -- 消耗历史
                // 2. 线程池没有退出 -- 正常工作
                t = _q.front(); // 取出第一个元素
                _q.pop();
            }
            t();
            LOG(LogLevel::DEBUG) << name << "handler task:" << t.Result2String();
        }
    }
};

最后是main函数,我们就种一个随机数种子来执行任务就行了~

7)main.cc

#include "Task.hpp"
#include "ThreadPool.hpp"
#include 
#include 
using namespace std;
int main()
{
    srand(time(nullptr) ^ getpid());
    EnableConsoleLogStrategy();
    unique_ptr> tp = make_unique>(10);
    //unique_ptr> tp2 = make_unique>(10);
    //unique_ptr> tp3 = make_unique>(10);
    tp->Start();
    int cnt = 10;
    while (cnt--)
    {
        // 生产任务
        int x = rand() % 10 + 1;
        usleep(rand() % 73);
        int y = rand() % 5 + 1;
        Task t(x, y);
        // push到线程池中,处理
        tp->Enqueue(t);
        sleep(1);
        // 如果线程池本身,被多线程访问呢?
    }
    tp->Stop();
    tp->Wait();
    return 0;
}

8)Makefile

这个正常写,只不过要用C++17来做

main:main.cc
	g++ -o $@ $^ -std=c++17 -lpthread
.PHONY:clean
clean:
	rm -f clean;

9)代码运行

接下来我们要将其改造一下~

2.单例模式

在Linux系统中,单例模式(Singleton Pattern)是一种常见的设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这种模式在系统编程中非常有用,尤其是在需要全局资源管理、配置管理或日志记录等场景中。

就像是正常的恋爱中,一个男生只能有一个女朋友一样~如果一个男生同时找了多个女朋友的话,可能会引发一系列的连锁问题!!!!严重时会导致恋情崩溃!!

那在很多服务器开发场景也是一样的, 我们经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要⽤⼀个单例的类来管理这些数据。而不是说找来一堆类来管理,这样会导致数据资源的竞争甚至说是进程的崩溃!!!
实现其有两种模式:懒汉模式和饿汉模式

2.1 懒汉模式

形象一点来说就是吃完饭, 先把碗放下, 然后下⼀顿饭⽤到这个碗了再洗碗, 就是懒汉模式。

如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊,初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。所以这种情况使用懒汉模式(延迟加载)更好。

代码示例:

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

可以看出,是用的时候再创建!!!(从那个if语句可以看出来)

2.2 饿汉模式

形象一点来说就是 吃完饭, 立刻洗碗, 这种就是饿汉模式. 因为下⼀顿吃的时候可以立刻拿着碗就能吃饭. 就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。
template 
class Singleton {
    static T data;
public:
    static T* GetInstance() {
        return &data;
    }
};

可以看出,不管用不用都创建了!!!

3.单例线程池

我们仅需要把ThreadPool.hpp文件重写一下即可,加入单例的代码~

我们将其命名为ThreadPoolSingle.hpp

ThreadPoolSingle.hpp

#pragma once
#include 
#include 
#include 
#include 
#include 
#include "Mutex.hpp"
#include "Cond.hpp"
#include "thread.hpp"
using namespace std;
// 单例线程池 - 懒汉模式
const static int defaultthreadnum = 3; // for debug
template 
class ThreadPool
{
public:
    void Start()
    {
        if (_isrunning)
            return;
        _isrunning = true;
        for (auto &t : _threads)
        {
            t.Start();
        }
        LOG(LogLevel::INFO) << "thread pool running success";
    }
    void Stop()
    {
        if (!_isrunning)
            return;
        _isrunning = false;
        if (_wait_thread_num)
            _cond.NotifyAll();
    }
    void Wait()
    {
        for (auto &t : _threads)
        {
            t.Join();
        }
        LOG(LogLevel::INFO) << "thread pool wait success";
    }
    void Enqueue(const T &t)
    {
        if (!_isrunning)
            return;
        {
            LockGuard lockguard(&_lock);
            _q.push(t);
            if (_wait_thread_num > 0)
                _cond.NotifyOne();
        }
    }
    // debug
    static std::string ToHex(ThreadPool *addr)
    {
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%p", addr);
        return buffer;
    }
    // 获取单例 ??
    static ThreadPool *GetInstance()
    {
        // A, B, c
        {
            // 线程安全,提高效率式的获取单例
            if(!_instance)
            {
                LockGuard lockguard(&_singleton_lock);
                if(!_instance)
                {
                    _instance=new ThreadPool();
                    LOG(LogLevel::DEBUG) << "线程池单例首次被使用,创建并初始化, addr: " <Start();
                }
            }
        }
        return _instance;
    }
    ~ThreadPool()
    {
    }
private:
    // 任务队列
    std::queue _q; // 整体使用的临界资源
    // 多个线程
    vector _threads; // 1. 创建线程对象 2. 让线程对象启动
    int _threadnum;
    int _wait_thread_num;
    // 保护机制
    Mutex _lock;
    Cond _cond;
    // 其他属性
    bool _isrunning;
    // 单例中静态指针
    static ThreadPool *_instance;
    static Mutex _singleton_lock;
    bool QueueIsEmpty()
    {
        return _q.empty();
    }
    void Routine(const string &name)
    {
        while (1)
        {
            T t;
            LockGuard lockguard(&_lock);
            while (QueueIsEmpty() && _isrunning)
            {
                _wait_thread_num++;
                _cond.Wait(_lock);
                _wait_thread_num--;
            }
            if (!_isrunning && QueueIsEmpty())
            {
                LOG(LogLevel::INFO) << " 线程池退出 && 任务队列为空, " << name << " 退出";
                break;
            }
            // 队列中一定有任务了!, 但是
            // 1. 线程池退出 -- 消耗历史
            // 2. 线程池没有退出 -- 正常工作
            t = _q.front();
            _q.pop();
            t();
            LOG(LogLevel::DEBUG) << name << " handler task: " << t.Result2String();
        }
    }
    ThreadPool(int threadnum = defaultthreadnum)
        : _threadnum(threadnum), _isrunning(false), _wait_thread_num(0)
    {
        for (int i = 0; i < _threadnum; i++)
        {
            // 方法1:
            // auto f = std::bind(hello, this);
            // 方法2
            string name = "thread-" + to_string(i + 1);
            _threads.emplace_back([this](const string &name)
                                  { this->Routine(name); }, name);
            // Thread t([this](){
            //     this->hello();
            // }, name);
            // _threads.push_back(st::move(t));
        }
        LOG(LogLevel::INFO) << "thread pool obj create success";
    }
    ThreadPool &operator=(const ThreadPool &) = delete;
    ThreadPool(const ThreadPool &) = delete;
};
// 静态成员变量需要在类外进行定义和初始化
template 
ThreadPool *ThreadPool::_instance = nullptr;
template 
Mutex ThreadPool::_singleton_lock;

main1.cc

#include "Task.hpp"
#include "ThreadPoolSingle.hpp"
#include 
#include 
int main()
{
    srand(time(nullptr) ^ getpid());
    EnableConsoleLogStrategy();
    int cnt = 10;
    while (cnt--)
    {
        // 生产任务
        int x = rand() % 10 + 1;
        usleep(rand() % 73);
        int y = rand() % 5 + 1;
        Task t(x, y);
        // push到线程池中,处理
        ThreadPool::GetInstance()->Enqueue(t);  //类的静态成员函数可以访问类中其他函数
        sleep(1);
    }
    ThreadPool::GetInstance()->Stop();
    ThreadPool::GetInstance()->Wait();
    return 0;
}

makefile

.PHONY:all
all:main main1
main:main.cc
	g++ -o $@ $^ -std=c++17 -lpthread
main1:main1.cc
	g++ -o $@ $^ -std=c++17 -lpthread
.PHONY:clean
clean:
	rm -f main main1;

运行一下:

搞定!!!

4.死锁

4.1 定义

死锁是计算机科学中一个常见的问题,特别是在并发编程和操作系统中。当两个或多个进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干预,它们都将无法向前推进,此时称系统处于死锁状态。

4.2 四个条件

4.2.1 互斥条件

资源不能被共享,即一个资源一次只能被一个进程使用。如果资源正在被使用,那么其他请求该资源的进程必须等待。

4.2.2请求与保持条件

进程至少占有一个资源,并且等待获取更多的资源,而这些资源正被其他进程占有

4.2.3 不可剥夺

已经分配给进程的资源不能被强制剥夺,只能由占有它的进程使用完后自行释放。

4.2.4 循环等待

存在一个进程等待环,其中每个进程都在等待下一个进程所占有的资源。即存在一个进程链,链中的每个进程都在等待下一个进程释放资源。

4.3 避免死锁

破坏互斥条件:允许资源被多个进程共享,但这可能会影响资源的独占性,导致其他问题。

破坏占有并等待条件:要求进程在开始执行前一次性申请其所需的全部资源。

破坏不可剥夺条件:允许资源被强制从进程中剥夺,但这可能会导致进程执行的不确定性。

破坏循环等待条件:为所有资源类型分配一个全局顺序,所有进程都必须按顺序请求资源。

posted @ 2025-10-29 11:17  clnchanpin  阅读(3)  评论(0)    收藏  举报