myserver-helloword-threadpool
铺垫概念:
- 进程:
- 定义:进程是操作系统分配资源和调度的基本单位。它是⼀个程序的实例,包含了执⾏程序的代码和活动路径。
- 特点:每个进程都有⾃⼰独⽴的地址空间,进程间的资源(如内存、⽂件句柄等)是隔离的。进程间通信(IPC)需要特定的机制,如管道、消息队列、共享内存等。
- 资源消耗:进程的创建、销毁以及上下⽂切换通常⽐线程更消耗资源,因为它们涉及更多的系统资源,包括内存分配、加载程序等。
- 线程:
- 定义:线程是进程内的⼀个执⾏单元,是CPU调度和分派的基本单位。它⽐进程更轻量级,可以在进程内并发执⾏。
- 特点:同⼀进程内的线程共享该进程的资源,如内存、⽂件句柄等。线程间的通信和数据交换相对更容易,因为它们共享相同的地址空间。
- 资源消耗:线程的创建和销毁、以及上下⽂切换的资源消耗相对较⼩。
区别:
- 资源分配与独⽴性:进程是资源分配的单位,每个进程拥有独⽴的地址空间;线程是CPU调度的单位,是进程的⼀部分,多个线程共享同⼀进程的资源。
- 通信⽅式:进程间通信需要特定的机制,相对复杂;线程间由于共享内存,通信更为简便。
- 开销⼤⼩:创建、销毁进程的开销⼤于线程,进程间的切换开销也⼤于线程间的切换。
引⼊线程池
由于主线程既要负责监听和管理事件,⼜要处理实际的任务,这可能会导致瓶颈,尤其是在⾼并发场景下。
引⼊线程池后的优化
引⼊线程池后,服务器的主线程主要负责事件监听和⼯作分发,⽽把耗时的任务处理交给线程池中的⼯作线程。这样做的具体流程如下:
- 检测新连接:主线程监听新的连接请求,检测到新连接时建⽴连接。
- 注册可读事件:主线程将该连接的可读事件注册到事件循环中,然后⽴即返回,继续监听其他事件或新连接。
- 从就绪列表取出事件:当连接的数据准备好被读取时,可读事件被触发,加⼊就绪列表。
- 分发任务到线程池:主线程从就绪列表中取出事件后,将实际的任务(如读取数据、解析请求、处理业务逻辑等)交给线程池中的⼀个⼯作线程来处理。主线程继续返回到事件循环中,不阻塞等待任务的处理完成。
- ⼯作线程处理具体任务:线程池中的⼯作线程接⼿后,读取内核缓冲区中的数据,解析请求,处理业务逻辑,⽣成响应等。
- 发送响应:处理完请求后,⼯作线程可能会通过事件循环,将响应数据异步发送回客⼾端。
线程池的优点
- 降低主线程压⼒:主线程只负责事件循环和任务分发,⽽不需要处理每个具体任务,确保主线程⾼效地处理⼤量并发连接。
- 提⾼并发性能:引⼊线程池后,多个⼯作线程可以并⾏处理具体的任务请求,⼤⼤提⾼了并发能⼒,减少了主线程被耗时任务阻塞的可能。
- 控制资源使⽤:线程池中的线程数量是可控的,避免了为每个连接创建新线程导致的资源开销过⼤。线程池也可以重⽤线程,降低线程的创建和销毁成本。
线程池
基本概念
- 线程池是⼀组预先初始化并且可重⽤的线程集合,⽤于执⾏多个任务。这种⽅法⽐为每个任务
单独创建和销毁线程更加⾼效。
线程池的核⼼组件
- 任务队列(Task Queue / Work Queue)
- 定义:⼀个⽤于存储待执⾏任务的队列。
- 作⽤:当有新的任务到来时,线程池不会直接为其创建线程,⽽是将任务添加到任务队列中,等待线程池中的线程来获取和执⾏。
- 调度机制:任务队列可以是FIFO(先进先出)队列、优先级队列等,调度机制决定了线程池如何选择要执⾏的任务。
- ⼯作线程(Worker Threads)
- 定义:线程池中预先创建的、实际执⾏任务的线程集合。
- 作⽤:这些线程从任务队列中取出任务并执⾏它们。在执⾏完⼀个任务后,它们会继续从任务队列中获取下⼀个任务,⽽不是销毁⾃⾝。
- 线程数控制:线程池通常允许设置线程的最⼩数量和最⼤数量,以及闲置线程的存活时间。当任务数多时,线程池会创建更多线程来处理任务;当任务数少时,空闲的线程会被销毁或挂起。
- 任务(Task / Job)
- 定义:需要被线程池处理的⼯作单元,可以是任何可执⾏的代码块,如函数、对象⽅法等。
- 作⽤:任务是线程池的基本处理对象。当新的任务提交给线程池时,任务会被添加到任务队列中,等待被⼯作线程执⾏。
- 线程池管理器(Thread Pool Manager)
- 定义:负责管理整个线程池的⽣命周期、任务分配、线程创建与销毁等⼯作。
- 作⽤:线程池管理器监控线程池的状态(如⼯作线程数量、任务队列⻓度、线程空闲时间等),并做出相应的调整。它的功能包括:
- 线程管理:根据任务数量和配置,决定是否增加或减少线程。
- 任务分配:将任务从任务队列分配给空闲的⼯作线程。
- 错误处理:处理线程运⾏中的异常、失败的任务等。
线程池的⼯作流程
- 初始化:线程池初始化时,创建⼀定数量的⼯作线程,通常是最⼩线程数。
- 任务提交:当新的任务提交到线程池时,线程池会将任务放⼊任务队列中。
- 任务分配:空闲的⼯作线程会从任务队列中取出任务,并执⾏任务代码。
- 任务执⾏:⼯作线程完成任务后,不会销毁,⽽是继续从任务队列中取出新的任务执⾏。如果任务队列为空,则进⼊空闲状态,直到有新的任务到来。
- 线程数量调整:线程池管理器根据当前任务量和线程使⽤情况,动态调整线程数量(增加、减少线程),以适应并发需求。
其他重要概念
创建线程:
- 在 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)的通⽤类型。这⾥的语法知识要点如下:
- std::function:
- std::function 是⼀种泛型类模板,它提供了⼀种类型安全的⽅式来存储、传递和调⽤不同类型的可调⽤对象。
- 它能够接受任意符合其签名要求的函数指针、lambda 表达式、bind 表达式结果以及重载了
operator() 的类实例(即函数对象)。
- void():
- 这部分是 std::function 类模板的参数化部分,它定义了可调⽤对象的类型。
- 在这个例⼦中,“void()”表⽰的是⼀个⽆参数并且返回 void 的函数签名。
- void 表⽰该函数不返回任何值。
- 参数列表为空括号 “()`”,意味着此可调⽤对象在调⽤时不需要任何参数。
- 使⽤场景:
- 当你需要将某个函数或可调⽤实体作为⼀个类成员变量存储,或者作为函数参数传递时,使⽤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
// 普通函数
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;
}
锁和信号量
- std::mutex(互斥锁)
- 定义与初始化:
1
std::mutex queue_mutex; // 创建⼀个互斥锁对象
- 锁定与解锁:
- 使⽤ lock() ⽅法来获取锁,如果锁已经被其他线程持有,则当前线程会阻塞直到获取到锁
1
queue_mutex.lock();
- 使⽤ 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
46std::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();
}
}
- 为了防⽌忘记解锁导致死锁或资源泄露,可以使⽤ std::lock_guard 或std::unique_lock 。当它们超出作⽤域时,会⾃动调⽤ unlock() 释放锁
- std::condition_variable(条件变量)
定义与初始化:1
std::condition_variable condition;
等待特定条件:
条件变量通常与互斥锁⼀起使⽤,⽤于线程间同步,当满⾜特定条件时唤醒线程。通过调⽤wait() 函数,线程会释放互斥锁并进⼊休眠状态,直到被其他线程通过 notify_one()或 notify_all() 唤醒,并重新获得锁后继续执⾏。
1
2
3
4
5
6
7
8
9
10
11std::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
7void 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 则提供了基于条件的等待和唤醒机制,使得多线程编程中的复杂同步问题得以解决。
右值引⽤
- 值类别(Value Category)
在C++中,每个表达式都有⼀个值类别,分为两种:
- 左值(lvalue):具有持久存储位置的表达式,可以出现在赋值操作符的左边或右边,例如变量名、数组元素、解引⽤的指针等。
- 右值(rvalue):临时对象或者将要销毁的对象,不能作为左值使⽤,通常不会有⼀个固定的内存地址。包括字⾯量、函数返回值、运算结果等。
- 引⽤类型
- 左值引⽤(lvalue reference):表⽰为 T& ,只能绑定到左值上。左值引⽤允许你给⼀个已存在的对象起⼀个新的名字,并且通过这个新名字操作原有对象。
1
2int x = 10;
int& ref_to_x = x; // ref_to_x 是 x 的左值引⽤ - 右值引⽤(rvalue reference):表⽰为 T&& ,可以绑定到右值和即将被销毁的左值(通过std::move()转换)。主要⽬的是为了⽀持移动语义和完美转发。
- 移动语义与移动构造函数/移动赋值运算符
右值引⽤最核⼼的应⽤是实现资源的有效转移,⽽⾮复制。通过定义移动构造函数和移动赋值运算符,你可以“窃取”右值的资源,⽽不需要拷⻉这些资源,从⽽提⾼效率。1
2
3
4
5
6
7
8
9
10
11
12
13
14class 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指向⼀块⼤内存
}; - 完美转发
以传⼊参数的原始形式(左值或右值)将其传递给其他函数。
完美转发的主要⽬的是实现对可调⽤对象(如函数、lambda表达式、成员函数指针等)及其参数的⽆损传递,使得接收这些参数的⽬标函数可以按照传⼊参数的原始形式处理它们。
通过使⽤ std::forward 和模板参数推导,模板函数可以保持传⼊参数原有的左值引⽤或右值引⽤性质,从⽽决定是在⽬标函数内部进⾏拷⻉操作、移动操作还是直接使⽤。
不使⽤完美转发的⽰例:
1 |
|
1 |
|
在模板编程中,通过使⽤右值引⽤和 std::forward() ,可以实现完美转发,即能以传⼊参数的原始形式(左值或右值)将其传递给其他函数。
std::forward 是 C++11 中引⼊的⼀个类型转换⼯具,⽤于在模板函数中实现完美转发。它能够根据参数的类型特性(左值/右值、const/⾮ const)进⾏正确地转发,确保参数在转发时保持其原始类型特性
在模板函数中,它通常⽤于将函数参数传递给另⼀个函数,同时确保参数是左值时被传递为左值,是右值时被传递为右值。
1 | template<typename T> |
复杂语句
1 | auto task = std::make_shared<std::packaged_task<return_type()>>( |
bind
1 |
|
std::async 和 std::future
std::async 是 C++11 标准库中的异步执⾏函数,⽤于启动⼀个异步任务,可能在新线程中执⾏。
1 |
|
std::future是 C++11 标准库中的模板类,⽤于获取异步操作的结果。
- get() :获取异步操作的结果,若结果未准备好,会阻塞当前线程。
- wait() :等待异步操作完成,不获取结果。
- valid() :检查 future 是否包含有效的共享状态。
代码
1 |
|