【Linux】线程同步与互斥(四):线程池与任务管理

文章目录

    • Linux线程同步与互斥(四):线程池与任务管理
    • 一、为什么需要线程池
      • [1.1 传统线程使用的问题](#1.1 传统线程使用的问题)
      • [1.2 线程池的思想](#1.2 线程池的思想)
      • [1.3 线程池的组成](#1.3 线程池的组成)
    • 二、任务类设计
      • [2.1 任务的抽象](#2.1 任务的抽象)
      • [2.2 Task基类](#2.2 Task基类)
      • [2.3 具体任务示例](#2.3 具体任务示例)
    • 三、线程池实现
      • [3.1 ThreadPool类设计](#3.1 ThreadPool类设计)
      • [3.2 完整实现](#3.2 完整实现)
      • [3.3 关键点解析](#3.3 关键点解析)
        • [3.3.1 为什么ThreadRoutine是静态函数](#3.3.1 为什么ThreadRoutine是静态函数)
        • [3.3.2 如何通知线程退出](#3.3.2 如何通知线程退出)
        • [3.3.3 任务的生命周期](#3.3.3 任务的生命周期)
    • 四、线程池测试
      • [4.1 基本测试](#4.1 基本测试)
      • [4.2 压力测试](#4.2 压力测试)
    • 五、单例模式优化
      • [5.1 为什么要单例](#5.1 为什么要单例)
      • [5.2 单例模式实现](#5.2 单例模式实现)
        • [5.2.1 懒汉式(延迟初始化)](#5.2.1 懒汉式(延迟初始化))
        • [5.2.2 线程安全的懒汉式](#5.2.2 线程安全的懒汉式)
        • [5.2.3 饿汉式](#5.2.3 饿汉式)
      • [5.3 完整的单例线程池](#5.3 完整的单例线程池)
    • 六、实战案例:简单日志系统
      • [6.1 日志系统需求](#6.1 日志系统需求)
      • [6.2 日志级别](#6.2 日志级别)
      • [6.3 日志任务](#6.3 日志任务)
      • [6.4 日志接口](#6.4 日志接口)
      • [6.5 使用示例](#6.5 使用示例)
    • 七、线程池的优化方向
      • [7.1 动态调整线程数](#7.1 动态调整线程数)
      • [7.2 任务优先级](#7.2 任务优先级)
      • [7.3 任务超时控制](#7.3 任务超时控制)
      • [7.4 监控和统计](#7.4 监控和统计)
    • 八、本篇总结
      • [8.1 核心知识点](#8.1 核心知识点)
      • [8.2 最重要的理解](#8.2 最重要的理解)

Linux线程同步与互斥(四):线程池与任务管理

💬 重磅来袭 :前面三篇把互斥锁、条件变量、生产者消费者模型都讲清楚了,这些知识怎么用到实际项目中?这就是本篇的核心------线程池(ThreadPool)。线程池是生产者消费者模型的典型应用:用户提交任务是生产者,工作线程处理任务是消费者,任务队列作为中间容器。我们会从线程池的设计思想讲起,分析为什么需要线程池,然后一步步实现一个完整的、可用的线程池。同时会用单例模式让线程池全局可用,并实现一个简单的日志系统作为实战案例。学完这篇,你就能把多线程编程真正用到项目里了。

👍 点赞、收藏与分享:本篇包含完整的线程池实现、设计模式应用、工程实践技巧,是多线程编程的必学内容!如果对你有帮助,请点赞、收藏并分享!

🚀 循序渐进:从原理到设计,从简单实现到完善优化,一步步掌握线程池。


一、为什么需要线程池

1.1 传统线程使用的问题

先看一个传统的多线程处理方式:

cpp 复制代码
void process_task(Task task) {
    // 处理任务...
}

// 每来一个任务,就创建一个线程
void handle_request(Task task) {
    pthread_t tid;
    pthread_create(&tid, nullptr, 
                   [](void* arg) -> void* {
                       Task* t = (Task*)arg;
                       process_task(*t);
                       delete t;
                       return nullptr;
                   }, 
                   new Task(task));
    pthread_detach(tid);  // 分离线程,自动回收
}

int main() {
    while (true) {
        Task task = get_task();
        handle_request(task);  // 每个任务一个线程
    }
}

这种方式有几个严重问题:

bash 复制代码
问题1:创建销毁开销大
  - 创建线程:分配栈空间、设置TCB、内核调度
  - 销毁线程:回收资源、更新内核数据结构
  - 频繁创建销毁,性能很差
  
问题2:资源消耗不可控
  - 并发量大时,会创建大量线程
  - 每个线程默认栈空间8MB
  - 1000个线程 = 8GB内存
  - 系统可能撑不住
  
问题3:线程切换开销
  - 线程越多,上下文切换越频繁
  - CPU时间浪费在切换上
  - 真正的计算时间反而少
  
问题4:难以管理
  - 线程数量不确定
  - 无法控制并发度
  - 难以监控和调试

1.2 线程池的思想

线程池的核心思想很简单:预先创建一定数量的线程,重复使用它们来执行任务

bash 复制代码
传统方式:
  任务1 → 创建线程1 → 执行 → 销毁线程1
  任务2 → 创建线程2 → 执行 → 销毁线程2
  任务3 → 创建线程3 → 执行 → 销毁线程3

线程池方式:
  启动时:创建N个工驻)
  
  运行时:
  任务1 → 任务队列 → 线程1取出执行
  任务2 → 任务队列 → 线程2取出执行
  任务3 → 任务队列 → 线程3取出执行
  任务4 → 任务队列 → 线程1执行完再取
  ...
  
  关闭时:通知线程退出,回收资源

优点:

bash 复制代码
优点1:重用线程,避免反复创建销毁
  - 线程创建一次,执行多个任务
  - 性能提升明显
  
优点2:控制并发数
  - 线程数固定(如CPU核心数)
  - 避免资源耗尽
  - 避免过度切换
  
优点3:管理方便
  - 统一的任务提交接口
  - 可以监控队列长度
  - 可以动态调整(进阶)
  
优点4:隔离资源
  - 某个任务的逻辑错误不影响其他任务

📌 核心理解:线程池是一种"池化技术",和数据库连接池、内存池的思想一样------预先分配资源,重复使用,减少分配释放开销。

1.3 线程池的组成

bash 复制代码
线程池的三个核心组件:

1. 任务队列(Task Queue)
   - 存放待执行的任务
   - 生产者-消费者模型的"容器"
   - 需要线程安全

2. 工作线程(Worker Threads)
   - 固定数量的线程
   - 不断从队列取任务执行
   - 生产者-消费者模型的"消费者"

3. 管理接口
   - 提交任务(生产者接口)
   - 启动/停止线程池
   - 查询状态(可选)

流程图:

bash 复制代码
                    线程池
    ┌─────────────────────────────────────┐
    │                                     │
    │  用户提交任务                       │
    │     │                               │
    │     ↓                               │
    │  ┌─────────────────┐                │
    │  │  任务队列       │                │
    │  │  [T1][T2][T3]  │                │
    │  └─────────────────┘                │
    │     │     │     │                   │
    │     ├─────┼─────┤                   │
    │     ↓     ↓     ↓                   │
    │  [线程1][线程2][线程3]              │
    │     │     │     │                   │
    │     └─────┴─────┘                   │
    │        执行任务                     │
    └─────────────────────────────────────┘

二、任务类设计

2.1 任务的抽象

任务是线程池要执行的工作单元。怎么表示任务?有几种方式:

bash 复制代码
方式1:函数指针
  typedef void (*task_func_t)(void *arg);
  
  优点:简单直接
  缺点:只能传void*,不灵活

方式2:C++11 std::function
  std::function<void()>
  
  优点:灵活,可以绑定任何可调用对象
  缺点:需要C++11

方式3:自定义Task类
  class Task { virtual void run() = 0; };
  
  优点:面向对象,清晰
  缺点:每种任务要继承实现

这里用方式3,因为它更清晰,也方便扩展。

2.2 Task基类

Task.hpp:

cpp 复制代码
#pragma once
#include <iostream>
#include <string>

class Task
{
public:
    Task() {}
    virtual ~Task() {}
    
    // 纯虚函数,子类必须实现
    virtual void run() = 0;
};

2.3 具体任务示例

计算任务:

cpp 复制代码
class CalcTask : public Task
{
public:
    CalcTask(int x, int y, char op)
        : _x(x), _y(y), _op(op)
    {}
    
    void run() override
    {
        int result = 0;
        switch (_op) {
            case '+': result = _x + _y; break;
            case '-': result = _x - _y; break;
            case '*': result = _x * _y; break;
            case '/': result = (_y == 0) ? -1 : _x / _y; break;
            case '%': result = (_y == 0) ? -1 : _x % _y; break;
            default:
                std::cout << "未知操作" << std::endl;
                return;
        }
        std::cout << "计算: " << _x << " " << _op << " " 
                  << _y << " = " << result 
                  << " [线程:" << pthread_self() << "]" << std::endl;
    }
    
private:
    int _x;
    int _y;
    char _op;
};

IO任务:

cpp 复制代码
class IOTask : public Task
{
public:
    IOTask(const std::string& filename)
        : _filename(filename)
    {}
    
    void run() override
    {
        std::cout << "读取文件: " << _filename 
                  << " [线程:" << pthread_self() << "]" << std::endl;
        sleep(1);  // 模拟IO耗时
        std::cout << "文件读取完成: " << _filename << std::endl;
    }
    
private:
    std::string _filename;
};

📌 设计思路:Task是接口,具体任务继承它并实现run()。线程池只需要知道Task接口,不需要知道具体任务类型。这是面向对象的多态思想。


三、线程池实现

3.1 ThreadPool类设计

线程池需要哪些成员?

bash 复制代码
数据成员:
├─ 任务队列(BlockingQueue<Task*>)
├─ 工作线程数组(vector<pthread_t>)
├─ 线程数量(int)
└─ 运行标志(bool)

函数成员:
├─ 构造函数:设置线程数
├─ Start():启动线程池
├─ Stop():停止线程池
├─ PushTask():提交任务
└─ ThreadRoutine():线程执行函数(静态)

3.2 完整实现

ThreadPool.hpp:

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include "BlockingQueue.hpp"
#include "Task.hpp"

const int default_thread_num = 5;

class ThreadPool
{
public:
    ThreadPool(int thread_num = default_thread_num)
        : _thread_num(thread_num)
        , _is_running(false)
    {}
    
    // 启动线程池
    void Start()
    {
        if (_is_running) {
            std::cout << "线程池已经在运行" << std::endl;
            return;
        }
        _is_running = true;
        std::cout << "启动线程池,线程数: " << _thread_num << std::endl;

        // 创建工作线程
        for (int i = 0; i < _thread_num; i++) {
            pthread_t tid;
            int ret = pthread_create(&tid, nullptr, ThreadRoutine, this);
            if (ret != 0) {
                std::cerr << "创建线程失败" << std::endl;
                continue;
            }
            _threads.push_back(tid);
        }
        

        std::cout << "线程池启动成功" << std::endl;
    }
    
    // 提交任务
    void PushTask(Task *task)
    {
        if (!_is_running) {
            std::cout << "线程池未运行,无法提交任务" << std::endl;
            return;
        }
        _task_queue.Push(task);
    }
    
    // 停止线程池
    //约定:
	//Stop() 调用后,不允许再提交任务;否则行为未定义。
    void Stop()
    {
        if (!_is_running) {
            std::cout << "线程池未运行" << std::endl;
            return;
        }
        
        std::cout << "停止线程池..." << std::endl;
        _is_running = false;
        
        // 往队列中放空任务,唤醒所有等待线程
        for (int i = 0; i < _thread_num; i++) {
            _task_queue.Push(nullptr);
        }
        
        // 等待所有线程结束
        for (auto tid : _threads) {
            pthread_join(tid, nullptr);
        }
        
        _threads.clear();
        std::cout << "线程池已停止" << std::endl;
    }
    
    ~ThreadPool()
    {
        if (_is_running) {
            Stop();
        }
    }
    
private:
    // 线程执行函数(静态成员函数)
    static void *ThreadRoutine(void *arg)
    {
        ThreadPool *pool = (ThreadPool *)arg;
        
        while (true) {
            Task *task = nullptr;
            pool->_task_queue.Pop(&task);
            
            // nullptr 作为退出信号
            if (task == nullptr) {
                std::cout << "线程 " << pthread_self() 
                          << " 收到退出信号" << std::endl;
                break;
            }
            
            // 执行任务
            task->run();
            delete task;  // 执行完删除任务
        }
        
        std::cout << "线程 " << pthread_self() << " 退出" << std::endl;
        return nullptr;
    }
    
private:
    int _thread_num;                  // 线程数量
    std::vector<pthread_t> _threads;  // 线程ID数组
    BlockingQueue<Task*> _task_queue; // 任务队列
    std::atomic<bool> _is_running;//为了简化讲解,这里使用 atomic<bool> 作为运行标志
}              

3.3 关键点解析

3.3.1 为什么ThreadRoutine是静态函数
bash 复制代码
原因:pthread_create 的线程函数原型是:
void *(*start_routine)(void *)

它要求是一个普通函数指针或静态成员函数。

普通成员函数:
  - 隐含一个 this 指针参数
  - 签名是 void *(ClassName::*)(void *)
  - 类型不匹配

静态成员函数:
  - 没有 this 指针
  - 签名是 void *(*)(void *)
  - 可以作为线程函数

解决方案:
  - ThreadRoutine 声明为 static
  - 通过参数传入 this 指针
  - 在函数内部转换回 ThreadPool*
3.3.2 如何通知线程退出
bash 复制代码
问题:
  工作线程在 Pop() 上阻塞等待任务
  如何让它们退出?

方案1:设置标志位
  while (_is_running) {
      Pop(&task);
  }
  
  问题:线程可能正在 Pop() 阻塞
        修改 _is_running 它也不知道

方案2:往队列放N个特殊任务
  Push(nullptr);  // 空任务作为退出信号
  
  线程收到 nullptr 就退出

本实现用方案2,更可靠。

3.3.3 任务的生命周期
bash 复制代码
创建:
  Task *task = new CalcTask(1, 2, '+');

提交:
  pool.PushTask(task);  // 所有权转移给线程池

执行:
  task->run();  // 线程池的工作线程执行

销毁:
  delete task;  // 执行完立即删除

注意:
  提交后不要再访问task指针
  线程池负责delete

四、线程池测试

4.1 基本测试

test_threadpool.cpp:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include "ThreadPool.hpp"
#include "Task.hpp"

int main()
{
    srand(time(nullptr));
    
    // 创建线程池
    ThreadPool pool(3);  // 3个工作线程
    pool.Start();
    
    // 提交10个任务
    const char ops[] = "+-*/%";
    for (int i = 0; i < 10; i++) {
        int x = rand() % 100;
        int y = rand() % 100;
        char op = ops[rand() % 5];
        
        Task *task = new CalcTask(x, y, op);
        pool.PushTask(task);
        
        std::cout << "提交任务: " << x << " " << op << " " << y << std::endl;
        usleep(100000);  // 0.1秒
    }
    
    sleep(3);  // 等待任务执行完
    
    pool.Stop();
    
    return 0;
}

编译运行:

bash 复制代码
$_threadpool.cpp -o test -std=c++11 -lpthread
$ ./test
启动线程池,线程数: 3
线程池启动成功
提交任务: 83 + 86
计算: 83 + 86 = 169 [线程:140234567]
提交任务: 77 * 15
计算: 77 * 15 = 1155 [线程:140234568]
提交任务: 93 / 35
计算: 93 / 35 = 2 [线程:140234569]
提交任务: 86 % 92
计算: 86 % 92 = 86 [线程:140234567]
...
停止线程池...
线程 140234567 收到退出信号
线程 140234567 退出
线程 140234568 收到退出信号
线程 140234568 退出
线程 140234569 收到退出信号
线程 140234569 退出
线程池已停止

观察:

bash 复制代码
1. 3个线程轮流执行任务
2. 同一个线程执行多个任务
3. 没有创建新线程
4. 停止时所有线程正常退出

4.2 压力测试

cpp 复制代码
int main()
{
    ThreadPool pool(5);
    pool.Start();
    
    // 提交1000个任务
    for (int i = 0; i < 1000; i++) {
        Task *task = new CalcTask(i, i+1, '+');
        pool.PushTask(task);
    }
    
    sleep(10);
    pool.Stop();
    
    return 0;
}

运行观察CPU和内存占用:

bash 复制代码
$ ./test &
[1] 12345

$ top -p 12345
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM
12345 user      20   0   15000   2000    500 S   100   0.1

观察:
- VIRT 稳定(没有不断增长)
- 线程数固定为5
- CPU占用稳定
- 内存没有泄漏

📌 对比传统方式:如果每个任务创建一个线程,1000个任务就是1000个线程,内存暴涨,系统可能崩溃。线程池把线程数限制在5个,安全可控。


五、单例模式优化

5.1 为什么要单例

现在的线程池使用方式:

cpp 复制代码
ThreadPool pool(5);
pool.Start();

// 各个模块都要用线程池
module1_use_pool(&pool);
module2_use_pool(&pool);
module3_use_pool(&pool);

问题:

bash 复制代码
1. 到处传递 pool 指针,麻烦
2. 可能创建多个线程池实例,浪费资源
3. 难以全局管理

理想方式:

cpp 复制代码
// 任何地方都能直接获取线程池
ThreadPool::GetInstance()->PushTask(task);

这就是单例模式(Singleton)的作用。

5.2 单例模式实现

单例模式保证一个类只有一个实例,并提供全局访问点。

5.2.1 懒汉式(延迟初始化)
cpp 复制代码
class ThreadPool
{
public:
    // 获取单例
    static ThreadPool* GetInstance()
    {
        if (_instance == nullptr) {
            _instance = new ThreadPool();
            _instance->Start();
        }
        return _instance;
    }
    
    // 禁止拷贝和赋值
    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;
    
    // ... 其他成员函数 ...
    
private:
    // 构造函数私有
    ThreadPool(int thread_num = default_thread_num)
        : _thread_num(thread_num)
        , _is_running(false)
    {}
    
    static ThreadPool* _instance;
    
    // ... 其他成员变量 ...
};

// 静态成员初始化
ThreadPool* ThreadPool::_instance = nullptr;

问题:线程不安全!

bash 复制代码
情况:
  线程A和线程B同时调用 GetInstance()
  
时刻  线程A                    线程B
T1    if (_instance == nullptr)
T2                            if (_instance == nullptr)
T3    _instance = new ...
T4                            _instance = new ...
      
结果:创建了两个实例!
5.2.2 线程安全的懒汉式
cpp 复制代码
class ThreadPool
{
public:
    static ThreadPool* GetInstance()
    {
        // 双重检查锁定(Double-Checked Locking)
        if (_instance == nullptr) {
            pthread_mutex_lock(&_mutex);
            if (_instance == nullptr) {
                _instance = new ThreadPool();
                _instance->Start();
            }
            pthread_mutex_unlock(&_mutex);
        }
        return _instance;
    }
    
private:
    static ThreadPool* _instance;
    static pthread_mutex_t _mutex;
};

ThreadPool* ThreadPool::_instance = nullptr;
pthread_mutex_t ThreadPool::_mutex = PTHREAD_MUTEX_INITIALIZER;

为什么要两次检查?

bash 复制代码
第一次检查(锁外):
  - 避免每次都加锁
  - 已经创建后,直接返回

第二次检查(锁内):
  - 防止多个线程同时通过第一次检查
  - 在锁保护下再次确认

流程:
时刻  线程A                    线程B
T1    if (_instance == nullptr)  ✓
T2                            if (_instance == nullptr)  ✓
T3    lock
T4    if (_instance == nullptr)  ✓
T5    _instance = new ...
T6    unlock
T7                            lock
T8                            if (_instance == nullptr)  ✗
T9                            unlock
T10                           return _instance

补充:双重检查锁在 C++ 中不推荐,仅作思想展示

5.2.3 饿汉式
cpp 复制代码
class ThreadPool
{
public:
    static ThreadPool* GetInstance()
    {
        return _instance;
    }
    
private:
    // 程序启动时就创建(静态成员在main之前初始化)
    static ThreadPool* _instance;
};

// 在main函数执行前就创建
ThreadPool* ThreadPool::_instance = new ThreadPool();

特点:

bash 复制代码
优点:
在程序启动阶段完成初始化,通常不会有并发问题
实现简单

注意:
不属于 C++11 标准保证的"线程安全初始化"
存在静态初始化顺序问题(static initialization order fiasco)

在 C++11 之后,更推荐"函数内 static 对象"的单例写法,它由语言标准保证线程安全初始化,并且不存在静态初始化顺序问题。

cpp 复制代码
class ThreadPool
{
public:
    // 获取全局唯一实例(返回引用)
    static ThreadPool& GetInstance()
    {
        // C++11 保证:该对象的初始化是线程安全的
        static ThreadPool instance;
        return instance;
    }

    // 禁止拷贝和赋值
    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;

    // 对外接口示例
    void Start()
    {
        // 启动线程池
    }

    void Stop()
    {
        // 停止线程池
    }

private:
    // 构造函数私有,防止外部创建
    ThreadPool()
    {
        // 初始化线程池资源
    }

    ~ThreadPool()
    {
        // 释放资源(程序退出时自动调用)
    }
};

函数内 static 单例在第一次调用时才创建实例,具备懒汉式的按需初始化特性; 同时其初始化过程由 C++11

标准保证线程安全,不需要显式加锁, 避免了传统懒汉式的并发问题,也不存在饿汉式的静态初始化顺序风险, 是现代 C++ 中综合最优的单例实现。

5.3 完整的单例线程池

这里用的其实是C++11之前的饿汉式,工程上也没什么问题
ThreadPool.hpp:

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include "BlockingQueue.hpp"
#include "Task.hpp"

const int default_thread_num = 5;

class ThreadPool
{
public:
    // 获取单例
    static ThreadPool* GetInstance()
    {
        return _instance;
    }
    
    // 禁止拷贝和赋值
    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;
    
    void Start()
    {
        if (_is_running) return;
        
        for (int i = 0; i < _thread_num; i++) {
            pthread_t tid;
            pthread_create(&tid, nullptr, ThreadRoutine, this);
            _threads.push_back(tid);
        }
        
        _is_running = true;
    }
    
    void PushTask(Task *task)
    {
        _task_queue.Push(task);
    }
    
    void Stop()
    {
        if (!_is_running) return;
        
        _is_running = false;
        
        for (int i = 0; i < _thread_num; i++) {
            _task_queue.Push(nullptr);
        }
        
        for (auto tid : _threads) {
            pthread_join(tid, nullptr);
        }
        
        _threads.clear();
    }
    
    ~ThreadPool()
    {
        if (_is_running) {
            Stop();
        }
    }
    
private:
    ThreadPool(int thread_num = default_thread_num)
        : _thread_num(thread_num)
        , _is_running(false)
    {}
    
    static void *ThreadRoutine(void *arg)
    {
        ThreadPool *pool = (ThreadPool *)arg;
        
        while (true) {
            Task *task = nullptr;
            pool->_task_queue.Pop(&task);
            
            if (task == nullptr) break;
            
            task->run();
            delete task;
        }
        
        return nullptr;
    }
    
private:
    static ThreadPool* _instance;
    
    int _thread_num;
    std::vector<pthread_t> _threads;
    BlockingQueue<Task*> _task_queue;
    bool _is_running;
};

// 饿汉式:程序启动时创建
ThreadPool* ThreadPool::_instance = new ThreadPool();

使用方式:

cpp 复制代码
int main()
{
    // 启动线程池
    ThreadPool::GetInstance()->Start();
    
    // 任何地方都可以直接提交任务
    for (int i = 0; i < 10; i++) {
        Task *task = new CalcTask(i, i+1, '+');
        ThreadPool::GetInstance()->PushTask(task);
    }
    
    sleep(3);
    ThreadPool::GetInstance()->Stop();
    
    return 0;
}

📌 单例的优势:全局只有一个线程池实例,任何地方都能通过GetInstance()访问,不需要传递指针,使用方便。


六、实战案例:简单日志系统

6.1 日志系统需求

日志系统是几乎每个项目都需要的组件:

bash 复制代码
需求:
1. 能记录不同级别的日志(INFO, WARNING, ERROR)
2. 能输出到不同目标(控制台、文件)
3. 线程安全
4. 性能好(不能阻塞业务线程)

设计思路:
- 业务线程调用log函数
- log函数创建日志任务
- 提交给线程池异步处理
- 日志任务负责实际写入

6.2 日志级别

cpp 复制代码
enum LogLevel
{
    INFO = 0,
    WARNING,
    ERROR,
    FATAL
};

const char* level_to_string(LogLevel level)
{
    switch (level) {
        case INFO:    return "INFO";
        case WARNING: return "WARNING";
        case ERROR:   return "ERROR";
        case FATAL:   return "FATAL";
        default:      return "UNKNOWN";
    }
}

6.3 日志任务

LogTask.hpp:

cpp 复制代码
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <ctime>
#include "Task.hpp"

enum LogLevel { INFO = 0, WARNING, ERROR, FATAL };

const char* level_to_string(LogLevel level)
{
    switch (level) {
        case INFO:    return "INFO";
        case WARNING: return "WARNING";
        case ERROR:   return "ERROR";
        case FATAL:   return "FATAL";
        default:      return "UNKNOWN";
    }
}

class LogTask : public Task
{
public:
    LogTask(LogLevel level, const std::string& message)
        : _level(level), _message(message)
    {
        // 记录创建时间
        time_t now = time(nullptr);
        char buf[64];
        strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", localtime(&now));
        _timestamp = buf;
    }
    
    void run() override
    {
        // 格式:[时间] [级别] 消息
        std::string log = "[" + _timestamp + "] " +
                          "[" + std::string(level_to_string(_level)) + "] " +
                          _message;
        
        // 输出到控制台
        std::cout << log << std::endl;
        
        // 同时写入文件
        std::ofstream ofs("app.log", std::ios::app);
        if (ofs.is_open()) {
            ofs << log << std::endl;
            ofs.close();
        }
    }
    
private:
    LogLevel _level;
    std::string _message;
    std::string _timestamp;
};

6.4 日志接口

Logger.hpp:

cpp 复制代码
#pragma once
#include "ThreadPool.hpp"
#include "LogTask.hpp"

class Logger
{
public:
    static void Log(LogLevel level, const std::string& message)
    {
        Task *task = new LogTask(level, message);
        ThreadPool::GetInstance()->PushTask(task);
    }
    
    static void Info(const std::string& message)
    {
        Log(INFO, message);
    }
    
    static void Warning(const std::string& message)
    {
        Log(WARNING, message);
    }
    
    static void Error(const std::string& message)
    {
        Log(ERROR, message);
    }
    
    static void Fatal(const std::string& message)
    {
        Log(FATAL, message);
    }
};

// 宏定义,方便使用
#define LOG_INFO(msg)    Logger::Info(msg)
#define LOG_WARNING(msg) Logger::Warning(msg)
#define LOG_ERROR(msg)   Logger::Error(msg)
#define LOG_FATAL(msg)   Logger::Fatal(msg)

6.5 使用示例

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include "ThreadPool.hpp"
#include "Logger.hpp"

void do_something(int id)
{
    LOG_INFO("开始处理任务 " + std::to_string(id));
    
    sleep(1);  // 模拟工作
    
    if (id % 5 == 0) {
        LOG_WARNING("任务 " + std::to_string(id) + " 可能有风险");
    }
    
    if (id % 10 == 0) {
        LOG_ERROR("任务 " + std::to_string(id) + " 出错了");
    }
    
    LOG_INFO("任务 " + std::to_string(id) + " 完成");
}

int main()
{
    // 启动线程池
    ThreadPool::GetInstance()->Start();
    
    // 模拟多线程业务
    for (int i = 0; i < 20; i++) {
        do_something(i);
    }
    
    sleep(3);  // 等待日志写完
    
    ThreadPool::GetInstance()->Stop();
    
    return 0;
}

运行结果:

bash 复制代码
$ ./test
[2025-01-31 10:30:15] [INFO] 开始处理任务 0
[2025-01-31 10:30:16] [INFO] 任务 0 完成
[2025-01-31 10:30:16] [INFO] 开始处理任务 1
[2025-01-31 10:30:17] [INFO] 任务 1 完成
...
[2025-01-31 10:30:20] [INFO] 开始处理任务 5
[2025-01-31 10:30:21] [WARNING] 任务 5 可能有风险
[2025-01-31 10:30:21] [INFO] 任务 5 完成
...
[2025-01-31 10:30:25] [INFO] 开始处理任务 10
[2025-01-31 10:30:26] [WARNING] 任务 10 可能有风险
[2025-01-31 10:30:26] [ERROR] 任务 10 出错了
[2025-01-31 10:30:26] [INFO] 任务 10 完成

$ cat app.log
[2025-01-31 10:30:15] [INFO] 开始处理任务 0
[2025-01-31 10:30:16] [INFO] 任务 0 完成
...

优点:

bash 复制代码
1. 业务线程不阻塞
   - LOG_INFO只是创建任务并提交
   - 立即返回,不等待写入完成
   
2. 线程安全
   - 多个线程调用LOG_INFO
   - 通过线程池和队列保证安全
   
3. 使用简单
   - 一行宏就能记录日志
   - 不需要关心底层细节

📌 实战技巧:日志系统是线程池的典型应用。把IO操作(写文件)丢给线程池异步处理,业务线程不会被阻塞,性能更好。

说明:

真实日志系统中,通常会:

  • 单独使用一个日志线程
  • 或在写文件时加互斥锁

七、线程池的优化方向

7.1 动态调整线程数

现在的线程池线程数是固定的。进阶版本可以根据任务数量动态调整:

bash 复制代码
策略:
- 任务队列长度 > 某个阈值:增加线程
- 线程空闲时间 > 某个阈值:减少线程
- 设置最小/最大线程数

实现:
- 需要一个管理线程监控队列
- 需要支持线程的动态创建和销毁
- 需要处理并发控制

7.2 任务优先级

bash 复制代码
需求:
  有些任务很重要,要优先执行

实现:
  - 使用优先队列代替FIFO队列
  - Task类添加priority字段
  - 队列按优先级排序

7.3 任务超时控制

bash 复制代码
需求:
  某些任务不能执行太久

实现:
  - 给Task添加超时时间
  - 用额外的线程监控
  - 超时后取消任务(pthread_cancel)

7.4 监控和统计

bash 复制代码
有用的指标:
- 当前活跃线程数
- 队列长度
- 已完成任务数
- 平均任务耗时
- 线程利用率

实现:
  添加计数器和统计变量
  提供查询接口

八、本篇总结

8.1 核心知识点

1. 线程池的必要性

bash 复制代码
问题:
├─ 频繁创建销毁开销大
├─ 资源消耗不可控
├─ 线程切换开销大
└─ 难以管理

方案:
  预先创建固定数量线程
  重复使用执行任务

2. 线程池组成

bash 复制代码
三个核心:
├─ 任务队列(BlockingQueue)
├─ 工作线程(Worker Threads)
└─ 管理接口(Start/Stop/PushTask)

本质:
  生产者-消费者模型的应用

3. 实现要点

bash 复制代码
├─ Task抽象:纯虚基类,多态
├─ 线程函数:静态成员,传this指针
├─ 退出通知:nullptr作为特殊任务
└─ 任务生命周期:new提交,执行后delete

4. 单例模式

bash 复制代码
目的:全局唯一实例,方便访问

实现方式:
├─ 懒汉式:延迟创建,需要加锁
├─ 饿汉式:启动时创建,天然线程安全(推荐)
└─ 双重检查:懒汉式的优化

5. 实战应用

bash 复制代码
日志系统:
  业务线程 → 创建日志任务 → 线程池异步处理
  
优点:
├─ 业务线程不阻塞
├─ 线程安全
└─ 使用简单

8.2 最重要的理解

📌 线程池的精髓

bash 复制代码
1. 池化技术
   - 预先分配资源
   - 重复使用
   - 减少创建销毁开销
   
2. 生产消费模型
   - 用户提交任务:生产者
   - 工作线程执行:消费者
   - 任务队列:缓冲区
   
3. 控制并发度
   - 固定线程数
   - 避免资源耗尽
   - 避免过度切换
   
4. 解耦业务逻辑
   - 业务只管提交任务
   - 不关心如何执行
   - 易于维护和扩展

5. 实际应用广泛
   - Web服务器:处理HTTP请求
   - 数据库:处理查询
   - 游戏服务器:处理玩家消息
   - 日志系统:异步写入

💬 总结:这四篇把Linux多线程编程从基础到实战全部讲完了。从互斥锁保护临界资源,到条件变量实现线程同步,到生产者消费者模型解耦并发,再到线程池管理任务执行------一个完整的知识体系。掌握了这些,你就能在实际项目中灵活使用多线程技术,写出高性能、线程安全的并发程序。

👍 点赞、收藏与分享:如果这个系列对你有帮助,请点赞、收藏并分享!感谢阅读,祝你在多线程编程的路上越走越远!

相关推荐
wbs_scy2 小时前
C++:智能指针完全指南(原理、用法与避坑实战,从 RAII 到循环引用)
开发语言·c++·算法
u0109272712 小时前
C++中的对象池模式
开发语言·c++·算法
qinyia2 小时前
如何在服务器上查看网络连接数并进行综合分析
linux·运维·服务器·开发语言·人工智能·php
Alter12302 小时前
拆开“超节点”的伪装:没有内存统一编址,仍是服务器堆叠
运维·服务器
思麟呀2 小时前
进程间通信
linux·运维·服务器
老兵发新帖2 小时前
Ubuntu上使用企业微信
linux·ubuntu·企业微信
k_cik_ci2 小时前
什么是负载均衡?
服务器·网络·负载均衡
Source.Liu2 小时前
【沟通协作软件】腾讯云域名DDNS搭建Matrix家庭服务器 - 完整操作笔记
服务器·腾讯云
hwj运维之路2 小时前
超详细ubuntu22.04部署k8s1.28高可用(一)【多master+keepalived+nginx实现负载均衡】
运维·云原生·kubernetes·负载均衡