C++11 多线程与并发编程(4)
文章目录
线程池
线程池是一种并发编程技术,用于管理和重用线程,以减少线程创建和销毁的开销。
当需要执行一些异步任务时,可以将这些任务交给线程池,由线程池负责创建和管理线程,分配任务执行。
线程池的实现思路:
- 任务队列:存放待执行的任务
- 初始化时,创建一定数量的工作线程,并等待任务
- 提交任务:当有任务需要执行时,将任务放入任务队列
- 工作线程从任务队列中取出任务,并执行
- 完成任务:任务执行完毕后,线程池等待新的任务或关闭
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
class ThreadPool
{
public:
// 构造函数
ThreadPool(const int numThreads) : stop(false)
{
for(int i = 0; i < numThreads; i++){
// 往每个线程中添加一个lambda表达式,用于获取将来可能出现的任务
workers.emplace_back(
[this](){
while(true){
// 创建一个void类型无参的可调用对象
std::function<void()> task;
// 一个局部作用域
{
// 锁住任务队列
std::unique_lock<std::mutex> ulg(this->queue_mutex);
this->cv.wait(ulg, [this]{
return this->stop || !this->tasks.empty();
});
// 如果任务队列为空或者线程池终止,直接返回
if(this->stop && this->tasks.empty())
return;
// 从任务队列中获取队首的任务
task = std::move(this->tasks.front());
this->tasks.pop();
}
// 执行任务
task();
}
}
);
}
}
// 加入任务队列
template <class F>
void enqueue(F &&task)
{
// 局部作用域
{
// 任务队列互斥,所以需要先锁住
std::unique_lock<std::mutex> ulg(queue_mutex);
tasks.emplace(std::forward<F>(task));
}
// 通知线程池中的线程可以取任务了,任务会自动执行
cv.notify_one();
}
// 析构函数
~ThreadPool()
{
{
// 防止多个线程修改stop
std::unique_lock<std::mutex> ulg(queue_mutex);
stop = true;
}
// 通知所有线程
cv.notify_all();
for(std::thread &worker: workers)
// 所有任务执行完,线程池才会销毁
worker.join();
}
private:
// 线程池,设置有多少个线程在池中,可供驱使
std::vector<std::thread> workers;
// 存储任务
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex; // 任务队列属于共享数据
std::condition_variable cv;
bool stop;
};
void example_task(int id) {
std::cout<< "Task "<< id<< " is being processed by thread" << std::this_thread::get_id() << std::endl;
}
int main(){
ThreadPool pool(4); // 创建一个大小为4的线程池
for(int i = 0; i < 8; i++){
pool.enqueue([i]{
example_task(i);
});
}
return 0;
}
异步并发(C++ 11)
头文件<future>
std::future ——异步操作的结果
std::future是C++11中的一个模板类,用于表示异步操作的结果。它允许在一个线程中启动一个异步任务,然后在需要时获取其结果,或者等待其完成。
通俗的说:future一般用于存储异步任务的结果。(后面结合函数说明)
// 定义一个future对象
// T为异步任务返回的数据类型
std::future<T> future_name;
future.get():
- **用于返回异步任务的结果。**如果任务尚未完成,调用
get()将阻塞当前线程,直到异步任务完成并返回结果。 - 如果异步任务抛出异常,
get()会重新抛出该异常,可以使用try-catch()捕获。
future.wait():
- 用于等待与
std::future关联的异步操作完成(阻塞当前线程),但它不会获取异步操作的结果。
std::async ——执行异步函数
std::async是C++ 11引入的一种用于异步执行函数的方法。
即,std::async允许启动一个函数,然后在后台执行,同时可以继续执行其他任务,直到需要该函数的结果。
// 调用方式1。Func是函数名,Args是函数入参
future<T> future_name = async(Func, Args);
std::async将会返回一个std::future对象,这个future对象存储了异步操作的结果。(如果有的话)
// 调用方式2,使用枚举值指定启动策略
// 同样后面是传入函数与入参
future<T> future_name = async(launch_enum, func, Args);
future_name.get();
可以通过提供std::launch枚举值作为第一个参数指定启动策略
std::launch::async:指示任务应该立即在新线程上执行。(无论是否有其他线程可用,都会创建一个新线程)std::launch::deferred:指示任务应该延迟执行,直到调用std::future::get()或std::future::wait()时才执行。不会创建新线程,延迟执行可能在调用get()时在当前线程中执行。- 默认情况(不显式指定时):通常是
std::launch::async(各标准不同)
#include <iostream>
#include <future>
int func(int x)
{
std::cout << "is running " << std::endl;
return x * x;
}
int main()
{
// 启动一个异步线程
std::future<int> result = std::async(func, 5);
// 模拟执行别的操作
std::cout << "Doing something else..." << std::endl;
std::cout << "Doing something else..." << std::endl;
std::cout << "Doing something else..." << std::endl;
// 获取结果
int square = result.get();
std::cout << "Result : " << square << std::endl;
return 0;
}

可以看到,没有指定launch参数时,异步线程是立刻启动,在我们调用future.get()以前已经运行。
std::packaged_task——封装执行任务
std::packaged_task 允许封装一个可调用对象(如函数、函数对象或 lambda 表达式),并将其与 std::future 结合起来,实现异步任务的管理和结果获取。
通俗的说:packaged_task封装了一个方法,可以通过自身调用这个方法,同时还能存储这个方法的返回结果。
// 创建一个packaged_task 并绑定一个函数
// func 为可调用对象
// return_T 是返回类型,args_T是func的入参类型
std::packaged_task<return_T(args_T)> task(func);
创建一个packaged_task后,我们可以创建一个线程进行执行,也可以直接调用packaged_task的operator()。而其结果存储在绑定的future对象中
#include <iostream>
#include <future>
#include <thread>
int add(int a, int b)
{
return a + b;
}
int main()
{
// 用packaged_task封装一个函数
std::packaged_task<int(int, int)> task(add);
// 将future对象关联到task,存储task执行完成的结果
std::future<int> result = task.get_future();
// 执行方式1:将task传递给某线程
std::thread t(std::move(task), 10, 20);
t.join();
// 获取task结果
int sum = result.get();
std::cout << "10 + 20 = " << sum << std::endl;
// 一个packaged_task只能执行一次
std::packaged_task<int(int, int)> task_2(add);
std::future<int> result_2 = task_2.get_future();
// 执行方式2:直接调用packaged_task的operator()
task_2(20, 30);
int sum_2 = result_2.get();
std::cout << "20 + 30 = " << sum_2 << std::endl;
return 0;
}

std::promise——线程间异步通信
std::promise是C++11标准库中的一个类,通常与 std::future 配合使用,用于实现线程间的异步通信。promise可以在线程间传递结果,在其中一个线程设置结果,而在另一个线程获取结果。
i.e. promise的用法可以看做一个数据,一个用于在多个线程间交流的数据。
用法:
std::promise::set_value():将值设置到std::promise对象中std::promise::get_future():将其关联到一个future对象中。之后使用该future对象的get()来获取promise的值
下面代码示例,我们在一个线程中生成了一个随机数,并且将结果存储在promise对象里。
而在另一个线程,通过promise绑定的future对象,获取随机值的值,并使用。
实现了两个线程间的数据通信。
#include <iostream>
#include <future>
#include <thread>
#include <random>
void generateNumber(std::promise<int> &prom)
{
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dist(1, 100);
int num = dist(gen); // 生成随机数
std::cout << "Generated number: " << num << std::endl;
prom.set_value(num); // 将随机数设置到prom中
}
void calculateSquare(std::future<int> &fut)
{
// 获取随机数
// 如果获取随机数的线程还会完成,则会阻塞当前线程
int num = fut.get();
int square = num * num; // 计算平方数
std::cout << " Square of the number: " << square << std::endl;
}
int main()
{
std::promise<int> prom;
// 关联future对象和promise
std::future<int> fut = prom.get_future();
// 通过promise,实现了线程间的通信
std::thread t1(generateNumber, std::ref(prom));
std::thread t2(calculateSquare, std::ref(fut));
t1.join();
t2.join();
return 0;
}

综合说明与运用
std::future提供一个访问异步操作结果的机制;std::packaged_task包装的是一个异步操作;std::promise包装的是一个值;都是为了方便异步操作。
-
需要获取线程中的某个值,用std::promise。
-
需要获一个异步操作的返回值,这时就用std::packaged_task。
我们比较在主线程中线性调用三个函数,与采用异步并发方式的耗时
#include <iostream>
#include <chrono>
#include <future>
#include <thread>
// 函数1:计算一个数的平方
int square(int x)
{
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::milliseconds(500));
return x * x;
}
// 函数2:计算一个数的立方
int cube(int x)
{
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟耗时操作
return x * x * x;
}
// 函数3:计算两个数的和
int add(int a, int b)
{
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟耗时操作
return a + b;
}
int main()
{
// 记录当前时间
auto start = std::chrono::high_resolution_clock::now();
// 在主线程中线性执行三个函数
int result1 = square(5);
int result2 = cube(10);
int result3 = add(result1, result2);
auto end = std::chrono::high_resolution_clock::now();
// 记录线性执行三个函数的时间
std::chrono::duration<double> duration_linear = end - start;
std::cout << "Linear execution time: " << duration_linear.count() << " seconds" << std::endl;
// 使用异步操作执行这三个函数并计时
start = std::chrono::high_resolution_clock::now();
// 第一个函数
std::promise<int> prom_square;
std::future<int> fut_square = prom_square.get_future();
std::thread t_square([&prom_square]()
{ prom_square.set_value(square(5)); });
// 第二个函数
std::promise<int> prom_cube;
std::future<int> fut_cube = prom_cube.get_future();
std::thread t_cube([&prom_cube]()
{ prom_cube.set_value(cube(10)); });
auto add_lambda = [&fut_square, &fut_cube]()
{
int result1 = fut_square.get();
int result2 = fut_cube.get();
return add(result1, result2);
};
// 使用前两个结果进行add运算,封装到packaged_task对象中
std::packaged_task<int()> pt_add(add_lambda);
std::future<int> fut_add = pt_add.get_future();
t_square.join();
t_cube.join();
// 直接调用运行
pt_add();
int result_add = fut_add.get();
end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration_async = end - start;
std::cout << "Async execution time: " << duration_async.count() << " seconds" << std::endl;
return 0;
}

可以看出,异步并发更快,而且差不多节约0.5s。是因为在异步方式里,求平方和三次方的任务同时进行的,求和等待其运行完才进行计算。
原子操作
在多线程编程中,当多个线程同时访问共享资源时,如果没有合适的同步机制,就可能发生数据竞争,导致未定义行为。
原子操作是不可被中断的操作,即使在多线程环境下,也能确保操作的完整性,可以避免一些数据竞争问题。
头文件 <atomic>
在C++ 11中提供了std::atomic模板类,用于实现原子操作。std::atomic 类型的对象可以保证对其操作的原子性。
std::atomic支持常见的原子操作,包括赋值、加法、减法、位操作等。
std::atomic<int> counter(0); // 定义一个原子变量
counter.store(10); // 将值设为10
int value = counter.load(); // 读取值
counter.fetch_add(5); // 原子地执行加法操作
counter.fetch_sub(3); // 原子地执行减法操作
- 内存顺序:
std::atomic支持多种内存顺序,通过模板参数进行指定,默认使用的是std::memory_order_seq_cst(顺序一致性内存序)。
std::atomic<int> data(0);
// 设置内存顺序为 memory_order_relaxed
data.store(10, std::memory_order_relaxed);
// 设置内存顺序为 memory_order_acquire
int value = data.load(std::memory_order_acquire);
std::atomic_flag:是一种特殊的原子类型,只能进行测试和设置操作,通常用于实现自旋锁。
std::atomic_flag flag = ATOMIC_FLAG_INIT; // 初始化为未设置状态
flag.test_and_set(); // 设置标志并返回先前的值
flag.clear(); // 清除标志
一个简单的避免数据竞争的示例
#include<iostream>
#include<thread>
#include<atomic>
std::atomic<int> atomic_counter(0); // 定义一个原子变量
int normal_counter = 0; // 定义一个普通变量
// 对原子变量进行加法
void increment_atomic(){
for(int i = 0; i < 10000; ++i){
atomic_counter++;
}
}
// 对普通变量进行加法
void increment_normal(){
for(int i = 0; i < 10000; ++i){
normal_counter++;
}
}
int main() {
std::thread t1(increment_atomic);
std::thread t2(increment_atomic);
std::thread t3(increment_normal);
std::thread t4(increment_normal);
t1.join();
t2.join();
t3.join();
t4.join();
std::cout << "Final value of atomic_counter: " << atomic_counter << std::endl;
std::cout << "Final value of normal_counter: " << normal_counter << std::endl;
return 0;
}

附(常见内存顺序):
std::memory_order_relaxed(松散内存序):- 操作不需要任何同步和顺序保证。
- 可以用于不依赖于其他操作结果的场景,例如递增计数器。
std::memory_order_acquire(获取内存序):- 当前操作之前的所有读操作必须在当前操作完成之前完成。
- 适用于读取共享数据,确保在读取共享数据之后,后续操作不会被重排到读取之前。
std::memory_order_release(释放内存序):- 当前操作之后的所有写操作必须在当前操作之前完成。
- 适用于写入共享数据,确保在写入共享数据之前,先前的操作不会被重排到写入之后。
std::memory_order_acq_rel(获取释放内存序):- 结合了获取和释放内存序的特性,用于同时保证前序和后序的内存可见性。
- 适用于读取和写入共享数据,既保证前序操作的结果对当前线程可见,又保证后序操作的结果对其他线程可见。
std::memory_order_seq_cst(顺序一致性内存序):- 提供了最强的内存顺序保证,所有操作都按照程序中的顺序执行。
- 适用于需要确保严格顺序性的场景,例如原子计数或同步操作。
参考:
本文详细介绍了C++11中的多线程并发编程,涉及线程池的原理与实现、std::future处理异步结果、std::async执行异步函数、std::packaged_task封装任务、std::promise进行线程间通信以及原子操作的重要性。通过实例展示了这些技术在实际编程中的应用和性能优化。
8462

被折叠的 条评论
为什么被折叠?



