铺垫概念:

  1. 进程:
  • 定义:进程是操作系统分配资源和调度的基本单位。它是⼀个程序的实例,包含了执⾏程序的代码和活动路径。
  • 特点:每个进程都有⾃⼰独⽴的地址空间,进程间的资源(如内存、⽂件句柄等)是隔离的。进程间通信(IPC)需要特定的机制,如管道、消息队列、共享内存等。
  • 资源消耗:进程的创建、销毁以及上下⽂切换通常⽐线程更消耗资源,因为它们涉及更多的系统资源,包括内存分配、加载程序等。
  1. 线程:
  • 定义:线程是进程内的⼀个执⾏单元,是CPU调度和分派的基本单位。它⽐进程更轻量级,可以在进程内并发执⾏。
  • 特点:同⼀进程内的线程共享该进程的资源,如内存、⽂件句柄等。线程间的通信和数据交换相对更容易,因为它们共享相同的地址空间。
  • 资源消耗:线程的创建和销毁、以及上下⽂切换的资源消耗相对较⼩。

区别:

  1. 资源分配与独⽴性:进程是资源分配的单位,每个进程拥有独⽴的地址空间;线程是CPU调度的单位,是进程的⼀部分,多个线程共享同⼀进程的资源。
  2. 通信⽅式:进程间通信需要特定的机制,相对复杂;线程间由于共享内存,通信更为简便。
  3. 开销⼤⼩:创建、销毁进程的开销⼤于线程,进程间的切换开销也⼤于线程间的切换。

引⼊线程池

由于主线程既要负责监听和管理事件,⼜要处理实际的任务,这可能会导致瓶颈,尤其是在⾼并发场景下。

引⼊线程池后的优化

引⼊线程池后,服务器的主线程主要负责事件监听和⼯作分发,⽽把耗时的任务处理交给线程池中的⼯作线程。这样做的具体流程如下:

  1. 检测新连接:主线程监听新的连接请求,检测到新连接时建⽴连接。
  2. 注册可读事件:主线程将该连接的可读事件注册到事件循环中,然后⽴即返回,继续监听其他事件或新连接。
  3. 从就绪列表取出事件:当连接的数据准备好被读取时,可读事件被触发,加⼊就绪列表。
  4. 分发任务到线程池:主线程从就绪列表中取出事件后,将实际的任务(如读取数据、解析请求、处理业务逻辑等)交给线程池中的⼀个⼯作线程来处理。主线程继续返回到事件循环中,不阻塞等待任务的处理完成。
  5. ⼯作线程处理具体任务:线程池中的⼯作线程接⼿后,读取内核缓冲区中的数据,解析请求,处理业务逻辑,⽣成响应等。
  6. 发送响应:处理完请求后,⼯作线程可能会通过事件循环,将响应数据异步发送回客⼾端。

线程池的优点

  • 降低主线程压⼒:主线程只负责事件循环和任务分发,⽽不需要处理每个具体任务,确保主线程⾼效地处理⼤量并发连接。
  • 提⾼并发性能:引⼊线程池后,多个⼯作线程可以并⾏处理具体的任务请求,⼤⼤提⾼了并发能⼒,减少了主线程被耗时任务阻塞的可能。
  • 控制资源使⽤:线程池中的线程数量是可控的,避免了为每个连接创建新线程导致的资源开销过⼤。线程池也可以重⽤线程,降低线程的创建和销毁成本。

线程池

基本概念

  • 线程池是⼀组预先初始化并且可重⽤的线程集合,⽤于执⾏多个任务。这种⽅法⽐为每个任务
    单独创建和销毁线程更加⾼效。

线程池的核⼼组件

  1. 任务队列(Task Queue / Work Queue)
  • 定义:⼀个⽤于存储待执⾏任务的队列。
  • 作⽤:当有新的任务到来时,线程池不会直接为其创建线程,⽽是将任务添加到任务队列中,等待线程池中的线程来获取和执⾏。
  • 调度机制:任务队列可以是FIFO(先进先出)队列、优先级队列等,调度机制决定了线程池如何选择要执⾏的任务。
  1. ⼯作线程(Worker Threads)
  • 定义:线程池中预先创建的、实际执⾏任务的线程集合。
  • 作⽤:这些线程从任务队列中取出任务并执⾏它们。在执⾏完⼀个任务后,它们会继续从任务队列中获取下⼀个任务,⽽不是销毁⾃⾝。
  • 线程数控制:线程池通常允许设置线程的最⼩数量和最⼤数量,以及闲置线程的存活时间。当任务数多时,线程池会创建更多线程来处理任务;当任务数少时,空闲的线程会被销毁或挂起。
  1. 任务(Task / Job)
  • 定义:需要被线程池处理的⼯作单元,可以是任何可执⾏的代码块,如函数、对象⽅法等。
  • 作⽤:任务是线程池的基本处理对象。当新的任务提交给线程池时,任务会被添加到任务队列中,等待被⼯作线程执⾏。
  1. 线程池管理器(Thread Pool Manager)
  • 定义:负责管理整个线程池的⽣命周期、任务分配、线程创建与销毁等⼯作。
  • 作⽤:线程池管理器监控线程池的状态(如⼯作线程数量、任务队列⻓度、线程空闲时间等),并做出相应的调整。它的功能包括:
  • 线程管理:根据任务数量和配置,决定是否增加或减少线程。
  • 任务分配:将任务从任务队列分配给空闲的⼯作线程。
  • 错误处理:处理线程运⾏中的异常、失败的任务等。

线程池的⼯作流程

  1. 初始化:线程池初始化时,创建⼀定数量的⼯作线程,通常是最⼩线程数。
  2. 任务提交:当新的任务提交到线程池时,线程池会将任务放⼊任务队列中。
  3. 任务分配:空闲的⼯作线程会从任务队列中取出任务,并执⾏任务代码。
  4. 任务执⾏:⼯作线程完成任务后,不会销毁,⽽是继续从任务队列中取出新的任务执⾏。如果任务队列为空,则进⼊空闲状态,直到有新的任务到来。
  5. 线程数量调整:线程池管理器根据当前任务量和线程使⽤情况,动态调整线程数量(增加、减少线程),以适应并发需求。

其他重要概念

创建线程:

  • 在 ThreadPool 类的构造函数中创建固定数量的线程。这些线程在创建时进⼊等待状态,等
    待执⾏任务。

互斥锁(Mutex):

  • 互斥锁是⼀种同步原语,⽤于保护共享资源或临界区,确保在任何时刻只有⼀个线程可以访问这些资源。
  • 在多线程环境中,如果不使⽤互斥锁来保护共享数据,可能会导致竞态条件和数据损坏。
  • 使⽤ std::mutex 类可以创建互斥锁,通过 lock() 和 unlock() ⽅法来控制互斥锁的加锁和解锁。

条件变量(Condition Variable):

  • 条件变量⽤于线程间的通信和协同⼯作。它允许⼀个线程等待某个条件的发⽣,⽽其他线程可以在满⾜条件时通知等待线程。
  • std::condition_variable 是C++标准库中的条件变量类,⽤于实现线程的等待和唤 醒。常⻅的⽤法是结合互斥锁使⽤,等待线程在等待某个条件时调⽤ wait() ⽅法挂起,⽽其他线程在满⾜条件时调⽤ notify_one() 或 notify_all() ⽅法来通知等待线程继续执⾏。

线程执⾏任务:

  • 线程从任务队列中取出任务并执⾏。执⾏完任务后,线程不会结束,⽽是继续等待下⼀个任
    务。

线程池销毁

  • 在 ThreadPool 类的析构函数中,通知所有线程停⽌等待并完成当前任务,然后退出。

C++知识

std::function<>

std::function<void()> 是C++标准库中的⼀种类型,它表⽰可以存储和调⽤任何⽆参数且没有返回值的可调⽤对象(Callable Object)的通⽤类型。这⾥的语法知识要点如下:

  1. std::function:
  • std::function 是⼀种泛型类模板,它提供了⼀种类型安全的⽅式来存储、传递和调⽤不同类型的可调⽤对象。
  • 它能够接受任意符合其签名要求的函数指针、lambda 表达式、bind 表达式结果以及重载了
    operator() 的类实例(即函数对象)。
  1. void():
  • 这部分是 std::function 类模板的参数化部分,它定义了可调⽤对象的类型。
  • 在这个例⼦中,“void()”表⽰的是⼀个⽆参数并且返回 void 的函数签名。
    • void 表⽰该函数不返回任何值。
    • 参数列表为空括号 “()`”,意味着此可调⽤对象在调⽤时不需要任何参数。
  1. 使⽤场景:
  • 当你需要将某个函数或可调⽤实体作为⼀个类成员变量存储,或者作为函数参数传递时,使⽤std::function<void()> 可以使代码更加灵活,因为你可以在运⾏时决定具体执⾏哪个操作。
  • 例如,在事件处理、回调函数注册、多线程编程中的任务队列等场景下经常⽤到。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    #include <iostream>
    #include <functional>
    // 普通函数
    void regular_function() {
    std::cout << "This is a regular function." << std::endl;
    }
    // 函数对象(仿函数)
    struct Functor {
    void operator()() const {
    std::cout << "This is a functor." << std::endl;
    }
    };

    int main() {
    // 使⽤普通函数
    std::function<void()> func1 = regular_function;
    func1();
    // 使⽤ lambda 表达式
    std::function<void()> func2 = []() {
    std::cout << "This is a lambda expression." << std::endl;
    };
    func2();
    // 使⽤函数对象
    Functor functor;

    std::function<void()> func3 = functor;
    func3();

    // 使⽤成员函数
    struct MyClass {
    void member_function() {
    std::cout << "This is a member function." << std::endl;
    }
    };
    MyClass obj;
    // 需要使⽤ std::bind 绑定对象

    std::function<void()> func4 = std::bind(&MyClass::member_function,&obj);
    func4();
    return 0;
    }

锁和信号量

  1. std::mutex(互斥锁)
  • 定义与初始化:
    1
    std::mutex queue_mutex; // 创建⼀个互斥锁对象
  • 锁定与解锁:
    • 使⽤ lock() ⽅法来获取锁,如果锁已经被其他线程持有,则当前线程会阻塞直到获取到锁
      1
      queue_mutex.lock();
  • 使⽤ unlock() ⽅法释放锁,使其他等待该锁的线程有机会获取并执⾏临界区代码。
    1
    queue_mutex.unlock();
  • ⾃动管理锁的⽣命周期:
    • 为了防⽌忘记解锁导致死锁或资源泄露,可以使⽤ std::lock_guard 或std::unique_lock 。当它们超出作⽤域时,会⾃动调⽤ unlock() 释放锁
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      std::mutex queue_mutex; // 定义⼀个互斥锁
      int shared_counter = 0; // 共享资源

      // 线程任务函数,增加共享计数器
      void incrementCounter(int id) {
      for (int i = 0; i < 5; ++i) {
      // 获取锁
      queue_mutex.lock();
      // 临界区代码:访问共享资源
      ++shared_counter;
      std::cout << "Thread " << id << " incremented counter to " << shared_counter << std::endl;
      // 释放锁
      queue_mutex.unlock();
      }
      }

      std::mutex queue_mutex; // 定义⼀个互斥锁
      int shared_counter = 0; // 共享资源
      // 线程任务函数,增加共享计数器

      void incrementCounter(int id) {
      for (int i = 0; i < 5; ++i) {
      // 使⽤ lock_guard 获取锁
      std::lock_guard<std::mutex> guard(queue_mutex);
      // 临界区代码:访问共享资源
      ++shared_counter;
      std::cout << "Thread " << id << " incremented counter to " << shared_counter << std::endl;
      // 离开作⽤域时,lock_guard 会⾃动释放锁
      }
      }
      std::mutex queue_mutex; // 定义⼀个互斥锁
      int shared_counter = 0; // 共享资源
      // 线程任务函数,增加共享计数器
      void incrementCounter(int id) {

      for (int i = 0; i < 5; ++i) {
      // 使⽤ unique_lock 获取锁
      std::unique_lock<std::mutex> lock(queue_mutex);
      // 临界区代码:访问共享资源
      ++shared_counter;
      std::cout << "Thread " << id << " incremented counter to " << shared_counter << std::endl;

      // 离开作⽤域时,unique_lock 会⾃动释放锁
      // 或者可以选择提前解锁: lock.unlock();
      }
      }
  1. std::condition_variable(条件变量)
    定义与初始化:
    1
    std::condition_variable condition;
  • 等待特定条件:

  • 条件变量通常与互斥锁⼀起使⽤,⽤于线程间同步,当满⾜特定条件时唤醒线程。通过调⽤wait() 函数,线程会释放互斥锁并进⼊休眠状态,直到被其他线程通过 notify_one()或 notify_all() 唤醒,并重新获得锁后继续执⾏。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    std::mutex cv_mutex;
    std::condition_variable condition;

    void waitingThread() {
    std::unique_lock<std::mutex> lock(cv_mutex);
    while (!data_ready) { // 检查某个共享变量是否满⾜条件
    condition.wait(lock); // 条件不满⾜时等待通知
    }
    // 当条件满⾜时,这⾥可以安全地访问和修改数据
    processData();
    }

    通知等待线程:

  • notify_one() :唤醒⾄少⼀个正在等待此条件变量的线程(如果有多个线程在等待,会选择其中⼀个唤醒)。

    1
    2
    3
    4
    5
    6
    7
    void notifierThread() {
    // 更新数据或改变条件

    data_ready = true;
    std::lock_guard<std::mutex> guard(cv_mutex); // 获取锁
    condition.notify_one(); // 唤醒⼀个等待的线程
    }
  • notify_all() :唤醒所有正在等待此条件变量的线程。

1
condition.notify_all(); // 唤醒所有等待的线程

总结来说, std::mutex ⽤于实现互斥访问,⽽ std::condition_variable 则提供了基于条件的等待和唤醒机制,使得多线程编程中的复杂同步问题得以解决。

右值引⽤

  1. 值类别(Value Category)
    在C++中,每个表达式都有⼀个值类别,分为两种:
  • 左值(lvalue):具有持久存储位置的表达式,可以出现在赋值操作符的左边或右边,例如变量名、数组元素、解引⽤的指针等。
  • 右值(rvalue):临时对象或者将要销毁的对象,不能作为左值使⽤,通常不会有⼀个固定的内存地址。包括字⾯量、函数返回值、运算结果等。
  1. 引⽤类型
  • 左值引⽤(lvalue reference):表⽰为 T& ,只能绑定到左值上。左值引⽤允许你给⼀个已存在的对象起⼀个新的名字,并且通过这个新名字操作原有对象。
    1
    2
    int x = 10;
    int& ref_to_x = x; // ref_to_x 是 x 的左值引⽤
  • 右值引⽤(rvalue reference):表⽰为 T&& ,可以绑定到右值和即将被销毁的左值(通过std::move()转换)。主要⽬的是为了⽀持移动语义和完美转发。
  1. 移动语义与移动构造函数/移动赋值运算符
    右值引⽤最核⼼的应⽤是实现资源的有效转移,⽽⾮复制。通过定义移动构造函数和移动赋值运算符,你可以“窃取”右值的资源,⽽不需要拷⻉这些资源,从⽽提⾼效率。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class ResourceIntensiveClass {
    public:
    // 移动构造函数,接受⼀个右值引⽤参数
    ResourceIntensiveClass(ResourceIntensiveClass&& other) : data(std::move(other.data)) {
    other.data = nullptr; // 资源从other转移到当前对象,并清空other
    }
    // 移动赋值运算符
    ResourceIntensiveClass& operator=(ResourceIntensiveClass&& other) {
    std::swap(data, other.data); // 交换数据指针,相当于资源的移动
    return *this;
    }
    private:
    BigData* data; // 假设data指向⼀块⼤内存
    };
  2. 完美转发
    以传⼊参数的原始形式(左值或右值)将其传递给其他函数。
    完美转发的主要⽬的是实现对可调⽤对象(如函数、lambda表达式、成员函数指针等)及其参数的⽆损传递,使得接收这些参数的⽬标函数可以按照传⼊参数的原始形式处理它们。
    通过使⽤ std::forward 和模板参数推导,模板函数可以保持传⼊参数原有的左值引⽤或右值引⽤性质,从⽽决定是在⽬标函数内部进⾏拷⻉操作、移动操作还是直接使⽤。

不使⽤完美转发的⽰例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
// 打印左值引⽤
void print(int& x) {
std::cout << "Non-const Lvalue reference: " << x << std::endl;
}
// 打印 `const` 左值引⽤
void print(const int& x) {
std::cout << "Const Lvalue reference: " << x << std::endl;
}
// 模板包装函数
template <typename T>
void wrapper(T&& arg) {
print(arg); // 直接传递参数
}
int main() {
int a = 10;
const int b = 20;
wrapper(a); // 输出:Non-const Lvalue reference: 10
wrapper(b); // 输出:Non-const Lvalue reference: 20 (错误地调⽤了⾮`const`版本)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
// 处理函数,接受左值引⽤
void process(int& x) {
std::cout << "Lvalue processed: " << x << std::endl;
}
// 处理函数,接受右值引⽤
void process(int&& x) {
std::cout << "Rvalue processed: " << x << std::endl;
}
// 包装函数(没有完美转发)
template <typename T>
void wrapper(T&& arg) {
// 直接传递参数到 process 函数
process(arg);
}

int main() {
int a = 10;
// 传⼊左值
wrapper(a); // 输出:Lvalue processed: 10
// 传⼊右值
wrapper(20); // 输出:Lvalue processed: 20 (错误地调⽤了左值版本)
return 0;
}

在模板编程中,通过使⽤右值引⽤和 std::forward() ,可以实现完美转发,即能以传⼊参数的原始形式(左值或右值)将其传递给其他函数。
std::forward 是 C++11 中引⼊的⼀个类型转换⼯具,⽤于在模板函数中实现完美转发。它能够根据参数的类型特性(左值/右值、const/⾮ const)进⾏正确地转发,确保参数在转发时保持其原始类型特性
在模板函数中,它通常⽤于将函数参数传递给另⼀个函数,同时确保参数是左值时被传递为左值,是右值时被传递为右值。

1
2
3
4
5
6
7
template<typename T>
void forward_func(T&& arg) {
func_impl(std::forward<T>(arg)); // 完美转发arg到func_impl
}
// ...
void func_impl(ResourceIntensiveClass& obj) {...} // 对左值版本的处理
void func_impl(ResourceIntensiveClass&& obj) {...} // 对右值版本的处理

复杂语句

1
2
3
4
5
6
7
8
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward(f), std::forward(args)...)
);
/*
创建⼀个std::packaged_task实例,封装了参数化模板中的可调⽤对象f及其参数args。
通过std::bind将可调⽤对象与其参数绑定在⼀起,形成⼀个新的可调⽤实体,这个实体在调⽤时会执⾏原函数。
将此std::packaged_task实例封装到⼀个std::shared_ptr中,便于在多线程环境下安全地共享、调度和执⾏任务
*/

bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <functional> // std::bind
// 复杂的原始函数
void printMessage(const std::string& prefix, const std::string& message, const std::string& suffix) {
std::cout << prefix << message << suffix << std::endl;
}
int main() {
// 使⽤ std::bind 将 prefix 和 suffix 预设成 "Hello, " 和 "!"
auto sayHello = std::bind(printMessage, "Hello, ", std::placeholders::_1, "!");
// 现在 sayHello 只需要⼀个参数
sayHello("World"); // 输出:Hello, World!
sayHello("C++"); // 输出:Hello, C++!
return 0;
}

std::async 和 std::future

std::async 是 C++11 标准库中的异步执⾏函数,⽤于启动⼀个异步任务,可能在新线程中执⾏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <future>

int compute(int x) {
return x * x;
}

int main() {
// 启动异步任务
std::future<int> result = std::async(std::launch::async, compute, 10);
// 在需要结果时获取
std::cout << "Result: " << result.get() << std::endl;
return 0;
}

std::future是 C++11 标准库中的模板类,⽤于获取异步操作的结果。
- get() :获取异步操作的结果,若结果未准备好,会阻塞当前线程。
- wait() :等待异步操作完成,不获取结果。
- valid() :检查 future 是否包含有效的共享状态。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#include <vector>                    // 引入标准向量容器,用于存储工作线程
#include <queue> // 引入标准队列容器,用于存储待执行的任务
#include <thread> // 引入线程库,用于创建和管理线程
#include <mutex> // 引入互斥锁,用于确保线程安全
#include <condition_variable> // 引入条件变量,用于线程等待和通知
#include <functional> // 引入函数对象相关库,用于包装和执行任务
#include <future> // 引入future库,用于管理异步任务的结果


class ThreadPool {
private:
std::vector<std::thread> workers; // 存储工作线程
std::queue<std::function<void()>> tasks; // 存储任务队列
std::mutex queue_mutex; // 任务队列的互斥锁
std::condition_variable condition; // 条件变量用于线程等待
bool stop; // 停止标志,控制线程池的生命周期

public:
// 构造函数,初始化线程池
ThreadPool(size_t threads) : stop(false) {
// 创建指定数量的工作线程
for(size_t i = 0; i < threads; ++i) {
workers.emplace_back([this] {
while(true) {
std::function<void()> task;
{
// 创建互斥锁以保护任务队列
std::unique_lock<std::mutex> lock(this->queue_mutex);
// 使用条件变量等待任务或停止信号
this->condition.wait(lock, [this]{ return this->stop || !this->tasks.empty(); });
// 如果线程池停止且任务队列为空,线程退出
if(this->stop && this->tasks.empty()) return;
// 获取下一个要执行的任务
//task2 task3
task = std::move(this->tasks.front());
this->tasks.pop();
}
// 执行任务
task();
}
});
}
}

// 将函数调用包装为任务并添加到任务队列,返回与任务关联的 future
/*
这段代码是C++中用于创建一个异步任务处理函数模板的部分,该函数模板接收一个可调用对象F和一组参数Args...,
并返回一个与异步任务结果关联的std::future对象。以下是详细解释:

std::future<typename std::result_of<F(Args...)>::type>

std::future: C++标准库中的组件,用于表示异步计算的结果。
当你启动一个异步操作时,可以获取一个std::future对象,通过它可以在未来某个时间点查询异步操作是否完成,并获取其返回值。

typename std::result_of<F(Args...)>::type:
这部分是类型推导表达式,使用了C++11引入的std::result_of模板。
std::result_of可以用来确定给定可调用对象F在传入参数列表Args...时的调用结果类型。
在这里,它的作用是推断出函数f(args...)调用后的返回类型。

整体来看,这个返回类型的声明意味着enqueue函数将返回一个std::future对象,
而这个future所指向的结果数据类型正是由f(args...)调用后得到的类型。

using return_type = typename std::result_of<F(Args...)>::type;

using关键字在这里定义了一个别名return_type,它是对上述类型推导结果的引用。

在后续的代码中,return_type将被用来表示异步任务的返回类型,简化代码并提高可读性。
*/
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type> {
using return_type = typename std::result_of<F(Args...)>::type;
// 创建共享任务包装器
/*
1. std::packaged_task<return_type()>
std::packaged_task是一个C++标准库中的类模板,它能够封装一个可调用对象(如函数、lambda表达式或函数对象),
并将该对象的返回值存储在一个可共享的状态中。这里的return_type()表示任务的返回类型是无参数的,
并且有一个名为return_type的返回类型。

2. std::make_shared<std::packaged_task<return_type()>>
std::make_shared是一个工厂函数,用于创建一个std::shared_ptr实例,指向动态分配的对象。
在这里,它被用来创建一个指向std::packaged_task<return_type()>类型的实例的智能指针。
用std::make_shared的好处在于它可以一次性分配所有需要的内存(包括管理块和对象本身),从而减少内存分配次数并提高效率,同时确保资源正确释放。

3. std::bind(std::forward<F>(f), std::forward<Args>(args)...)
- std::bind函数从C++11开始引入,用于将可调用对象与一组给定的参数绑定在一起,
创建一个新的可调用对象。新的可调用对象可以在以后任意时刻以适当的方式调用原始可调用对象。
- 在这里,F代表传入的可调用对象类型,而Args代表可能有的参数类型列表。
- std::forward<F>(f)和std::forward<Args>(args)...是对完美转发的支持,
使得f和args能以适当的左值引用或右值引用的形式传递给std::bind,
这样可以保持原表达式的值类别信息,特别是当传递的是右值时,可以避免拷贝开销。
*/
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
// 获取与任务关联的 future
std::future<return_type> res = task->get_future();
{
// 使用互斥锁保护任务队列
std::unique_lock<std::mutex> lock(queue_mutex);
// 如果线程池已停止,则抛出异常
if(stop) throw std::runtime_error("enqueue on stopped ThreadPool");
// 将任务添加到队列
tasks.emplace([task](){ (*task)(); });
}
// 通知一个等待的线程去执行任务
condition.notify_one();
return res;
}

// 析构函数,清理线程池资源
~ThreadPool() {
{
// 使用互斥锁保护停止标志
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
// 唤醒所有等待的线程
condition.notify_all();
// 等待所有工作线程退出
for(std::thread &worker: workers) {
worker.join();
}
}


};