100 行 C++ 代码实现线程池 - 基础知识
一、实验介绍
1.1 实验内容
为了追求性能,在服务器开发中我们经常要面临大量线程任务之间的调度和管理,本次实验我们将使用 C++ 设计并实现一个简单的线程池库。
本课程介绍需要用到的库和基本原理
1.2 实验知识点
-
C++11 标准库特性
- std::thread
- std::mutex, std::unique_lock
- std::condition_variable
- std::future, std::packaged_task
- std::function, std::bind
- std::shared_ptr, std::make_shared
- std::move, std::forward
-
C++11 语言特性
- Lambda 表达式
- 尾置返回类型
-
线程池模型
- 测试驱动开发思想
1.3 适合人群
适合对于c++想深入学习,对于计算机操作系统想深入学习的同学,虽然代码不长但是设计的知识点很多,希望同学们慢慢消化。
二、实验原理
2.1 线程池简介
多线程技术主要是解决单个处理器单元内多个线程的执行问题,由此诞生了所谓的线程池技术。线程池主要由三个基本部分组成:
- 线程池管理器(Thread Pool):负责创建、管理线程池,最基本的操作为:创建线程池、销毁线程池、增加新的线程任务;
- 工作线程(Worker):线程池中的线程,在没有任务时会处于等待状态,可以循环执行任务;
- 任务队列(Tasks Queue):未处理任务的缓存队列。
简单来说,一个线程池负责管理了需要执行的多个并发执行的多个线程中可执行数量多的线程、以及他们之间的调度。
为了更深刻的理解线程池这项技术,我们来看一个实际点的例子。
在 Web 服务器中,如果一天中服务器需要处理一百万个请求,并且每个请求都需要让一个独立的线程完成。为了保证服务器任务执行的高效性(能执行的赶紧执行),不应该让并发执行的线程数无节制的增长,所以,线程池在这其中就发挥了作用。
考虑一个处理器完成一项任务会分为创建线程、线程执行任务和销毁线程三个阶段。
如果在某个访问高峰期同时出现了十万的并发请求,且每个任务的请求都很简单,甚至执行任务的时间还小于这个线程被创建的时间,那么这时处理器必须花费大量的时间来创建这些请求的线程,而很长时间内让各个线程得不到执行。
有了线程池之后,我们可以在程序启动后创建一定数量的线程,当任务到达后,缓冲队列会将任务加入到线程中进行执行,执行完成后,线程并不销毁,而是等待下一任务的到来。
2.2 基础知识
C++11 引入了非常丰富且有用的新特性,尤其是并发编程支持的大量新特性,这才使得我们能够在100行以内编写一个复杂的线程池成为可能。在设计编写线程池之前,我们先回顾一下我们可能会用到的这些特性。
我们将在接下来的篇幅中复习(学习)下面这些 C++11 的特性、泛型编程以及多线程中的并发模型(互斥锁),如果对这些内容较熟悉,可以直接跳过本节直接查看下一个实验:
- 语言特性
- lambda expression
- 尾置返回类型
- 右值引用
- 标准库特性
- std::thread
- std::mutex, std::unique_lock
- std::future, std::packaged_task
- std::condition_variable
- std::function, std::bind
- std::shared_ptr, std::make_shared
- std::move, std::forward
2.3 语言特性
1. Lambda 表达式
Lambda 表达式是 C++11中最重要的新特性之一,而 Lambda 表达式,实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。这样的场景其实有很多很多,所以匿名函数几乎是现代编程语言的标配。
Lambda 表达式的基本语法如下:
[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
// 函数体
}
上面的语法规则除了 [捕获列表]
内的东西外,其他部分都很好理解,只是一般函数的函数名被略去,返回值使用了一个 ->
的形式进行。
所谓捕获列表,其实可以理解为参数的一种类型,lambda 表达式内部函数体在默认情况下是不能够使用函数体外部的变量的,这时候捕获列表可以起到传递外部数据的作用。根据传递的行为,捕获列表也分为以下几种:
1. 值捕获
与参数传值类似,值捕获的前期是变量可以拷贝,不同之处则在于,被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝:
void learn_lambda_func_1() {
int value_1 = 1;
auto copy_value_1 = [value_1] {
return value_1;
};
value_1 = 100;
auto stored_value_1 = copy_value_1();
// 这时, stored_value_1 == 1, 而 value_1 == 100.
// 因为 copy_value_1 在创建时就保存了一份 value_1 的拷贝
}
2. 引用捕获
与引用传参类似,引用捕获保存的是引用,值会发生变化。
void learn_lambda_func_2() {
int value_2 = 1;
auto copy_value_2 = [&value_2] {
return value_2;
};
value_2 = 100;
auto stored_value_2 = copy_value_2();
// 这时, stored_value_2 == 100, value_1 == 100.
// 因为 copy_value_2 保存的是引用
}
3. 隐式捕获
手动书写捕获列表有时候是非常复杂的,这种机械性的工作可以交给编译器来处理,这时候可以在捕获列表中写一个 &
或 =
向编译器声明采用 引用捕获或者值捕获.
总结一下,捕获提供了lambda 表达式对外部值进行使用的功能,捕获列表的最常用的四种形式可以是:
- [] 空捕获列表
- [name1, name2, ...] 捕获一系列变量
- [&] 引用捕获, 让编译器自行推导捕获列表
- [=] 值捕获, 让编译器执行推导应用列表
2.尾置返回类型
有时候,当希望编写一个函数来接收某个序列容器中返回的一个元素的应用时候,你可能就不太能够想明白应该如何写出这个函数的返回值类型了:
template <typename T>
return_type &getItem(T begin, T end) {
return *begin; // 返回序列中一个元素的引用
}
这里的 return_type
应该怎么写呢?事实上,我们可能会想到使用 decltype()
来获得这个类型,但是,编译器在读到这个函数定义的时候,begin 甚至还没有出现,这时候我们似乎没有任何办法直接在返回类型的时候写下这个返回类型。
C++11 提供了一种新的书写返回值的方式,那就是将返回类型尾置。尾置的返回类型允许我们在参数列表之后申明返回的类型,我们的代码可以写成:
template <typename T>
auto &getItem(T begin, T end) -> decltype(*begin) {
return *begin; // 返回序列中一个元素的引用
}
其中,我们使用 decltype 告知了编译器返回类型与参数表中的返回类型相同,而 decltype 会自动推断为元素类型的引用,完成了我们的需求。
当然,并不是只有这种情况才能够使用尾置返回类型,任函数都可以这么干,这种写法的好处在于能够让我们的返回类型变得清晰,以至于我们不会被各种复杂的返回类型搞得头晕,例如:
```cpp
int (*)[5]func(in
本课程为会员专属,查看完整内容请