【C++11多线程与并发编程】 (4) 线程池、<future>异步并发编程与<atomic>原子操作

本文详细介绍了C++11中的多线程并发编程,涉及线程池的原理与实现、std::future处理异步结果、std::async执行异步函数、std::packaged_task封装任务、std::promise进行线程间通信以及原子操作的重要性。通过实例展示了这些技术在实际编程中的应用和性能优化。

C++11 多线程与并发编程(4)


C++11多线程与并发编程

线程池

线程池是一种并发编程技术,用于管理和重用线程,以减少线程创建和销毁的开销。

当需要执行一些异步任务时,可以将这些任务交给线程池,由线程池负责创建和管理线程,分配任务执行。

线程池的实现思路:

  • 任务队列:存放待执行的任务
  • 初始化时,创建一定数量的工作线程,并等待任务
  • 提交任务:当有任务需要执行时,将任务放入任务队列
  • 工作线程从任务队列中取出任务,并执行
  • 完成任务:任务执行完毕后,线程池等待新的任务或关闭
#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;
}

image-20240420233618463

执行结果

可以看到,没有指定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;
}

image-20240421120014290

执行结果

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;
}

image-20240421160057897

运行结果

综合说明与运用

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;
}

image-20240421181523553

可以看出,异步并发更快,而且差不多节约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;
}

image-20240421190232452

无论多少次atomic_counter的结果都为20000,而normal_counter则不一定

附(常见内存顺序):

  1. std::memory_order_relaxed(松散内存序)
    • 操作不需要任何同步和顺序保证。
    • 可以用于不依赖于其他操作结果的场景,例如递增计数器。
  2. std::memory_order_acquire(获取内存序)
    • 当前操作之前的所有读操作必须在当前操作完成之前完成。
    • 适用于读取共享数据,确保在读取共享数据之后,后续操作不会被重排到读取之前。
  3. std::memory_order_release(释放内存序)
    • 当前操作之后的所有写操作必须在当前操作之前完成。
    • 适用于写入共享数据,确保在写入共享数据之前,先前的操作不会被重排到写入之后。
  4. std::memory_order_acq_rel(获取释放内存序)
    • 结合了获取和释放内存序的特性,用于同时保证前序和后序的内存可见性。
    • 适用于读取和写入共享数据,既保证前序操作的结果对当前线程可见,又保证后序操作的结果对其他线程可见。
  5. std::memory_order_seq_cst(顺序一致性内存序)
    • 提供了最强的内存顺序保证,所有操作都按照程序中的顺序执行。
    • 适用于需要确保严格顺序性的场景,例如原子计数或同步操作。

参考:

[1] 面试必考的:并发和并行有什么区别?

[2] C++网络编程高并发 讲师:陈子青

[3] cppreference——thread

[4] cppreference——mutex

[5] 帝江VII ——c++11 call_once用法(多线程时仅初始化一次的完美解决方案)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值