前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【C++11】std::async函数介绍及问题梳理

【C++11】std::async函数介绍及问题梳理

作者头像
SarPro
发布2024-05-15 08:16:25
5090
发布2024-05-15 08:16:25
举报
文章被收录于专栏:【计网】Cisco
目录

🌞1. std::async 简介

🌞2. 问题梳理

🌊2.1 std::async(异步执行) 到 future get 直接调用会如何抛异常

🌊2.2 std::async 如果通过劫持让 new 内存不够,有没有可能抛异常

🌊2.3 std::async 如果系统线程不够有没有可能异常

🌞3. gdb调试async详情

🌊3.1 模拟调用 new 失败场景

🌊3.2 模拟调用 linux api 失败场景


🌞1. std::async 简介

std::async 是 C++11 标准库中用于异步执行的函数,会返回一个 std::future 对象,以获取函数的执行结果。可用其在新线程中执行函数,也可以在当前线程中执行。std::async 的函数声明形式通常如下:

代码语言:javascript
复制
template <typename F, typename... Args>
std::future<typename std::result_of<F(Args...)>::type> 
    std::async(std::launch policy, F&& f, Args&&... args);

说明:

  • template <typename F, typename... Args>:函数模板声明。F 是要执行的函数类型,Args... 是函数参数类型的模板参数包【这意味着函数可以接受任意数量的参数】
  • std::future<typename std::result_of<F(Args...)>::type>:这是 std::async 函数的返回类型。它是一个 std::future 对象,包装了函数 F 的返回类型。std::future 允许在未来的某个时间点获取函数的执行结果。
  • std::async(std::launch policy, F&& f, Args&&... args):这是函数 std::async 的声明。它接受三个参数:
    • policystd::launch 类型的参数,表示函数执行的策略,有如下2种: std::launch::async(在新线程中异步执行) std::launch::deferred(延迟执行,在调用 std::future::get()std::future::wait() 时执行)。
    • f:通用引用(universal reference),表示要执行的函数对象。通用引用允许 f 接受任意类型的参数。
    • args:这是函数 f 的参数列表。可以是零个或多个参数。
  • 这个函数的作用是根据给定的执行策略异步执行函数 f,并返回一个 std::future 对象,可用来等待函数的执行完成并获取函数的结果。

注意:

std::async 的行为受到执行策略参数 std::launch 类型的参数的影响,可能在调用时立即执行,也可能延迟到 std::future::get()std::future::wait() 被调用时才执行。


🌞2. 问题梳理

🌊2.1 std::async(异步执行) 到 future get 直接调用会如何抛异常

std::asyncstd::future::get 直接调用会抛出异常,主要有两种情况:

  1. 函数对象抛出异常。
  2. 如果使用 std::launch::async 策略,并在调用 std::future::get 之前的函数执行抛出了异常,这种情况下会导致 std::future::get 抛出 std::future_error 异常。

【示例1】函数对象抛出异常

代码语言:javascript
复制
#include <iostream>
#include <future>

// 抛出异常
void task1() {
    throw std::runtime_error("An error occurred in task1()");
}

int main() {
    try {
        // 使用 std::async 启动一个异步任务
        auto future1 = std::async(std::launch::async, task1);

        // 等待异步任务的完成并获取结果
        future1.get(); // 这里会抛出异常
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }

    return 0;
}

//输出内容:
Caught exception:An error occurred in task1()

该示例中,task1 函数会抛出异常。当调用 future1.get() 时,如果 task1 函数抛出异常,std::future::get 也会抛出异常。

【示例2】使用 std::launch::async 策略并在函数执行前抛出异常

代码语言:javascript
复制
#include <iostream>
#include <future>
#include <thread>
#include <chrono>

// 在函数执行前抛出异常的函数
void task2() {
    // 人为延迟,增加在调用 std::future::get 前抛出异常的机会
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    throw std::runtime_error("An error occurred in task2()");
}

int main() {
    try {
        // 使用 std::async 启动一个异步任务,使用 std::launch::async 策略
        auto future2 = std::async(std::launch::async, task2);

        // 在get函数执行前抛出异常
        throw std::runtime_error("An error occurred before calling future2.get()");

        // 等待异步任务的完成并获取结果
        future2.get(); // 这里会抛出 std::future_error 异常
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }

    return 0;
}

//输出内容:
Caught exception: An error occurred before calling future2.get()

该示例中,task2 函数会在std::future::get 函数执行前抛出异常。

main 函数中,虽然调用 future2.get() 前手动抛出了异常,但是由于使用了 std::launch::async 策略,task2 函数会在新线程中执行std::future::get() 调用则在当前线程(即主线程)中执行】。因此,即使在主线程中抛出了异常,新线程中的任务函数也会继续执行:std::future::get 会等待 task2 函数执行完成【含加入的延时:100毫秒】,然后抛出 std::future_error 异常,说明在获取结果之前已经发生异常。


🌊2.2 std::async 如果通过劫持让 new 内存不够,有没有可能抛异常

std::async 不会直接抛出异常来处理内存不足的情况。在 C++ 中,当 new 操作符无法分配所需的内存时,会抛出 std::bad_alloc 异常,但std::async 不会直接抛出该异常。

std::async 中,任务可能在一个新线程中执行,也可能在当前线程中执行。如果任务在新线程中执行,并且在该新线程中发生了内存分配失败,那么系统会终止整个程序,而不是将异常传递回调用 std::async 的地方【这是因为线程的异常不能跨线程传递】

这是因为C++的异常处理机制不能跨线程传播。当一个异常在一个线程中被抛出,而没有被捕获时,它会导致这个线程终止。如果异常发生在 std::async 创建的新线程中,并且在那里没有被捕获,那么整个线程会终止,但异常不会被传递回调用 std::async 的线程。 所以,虽然劫持 new 可以模拟内存不足的情况,但由于异常处理机制的限制,std::async 并不能捕获由于新线程中的内存分配失败而导致的异常。

所以,如果在 std::async内部发生了内存分配失败,程序通常会终止并可能会生成错误报告,而不是抛出异常到 std::async 的调用者。因此,对于内存不足的情况,最好在程序中进行适当的内存管理和异常处理,而不是依赖于 std::async 来处理此类问题。

【示例1】系统内存不足导致异常

代码语言:javascript
复制
#include <iostream>
#include <future>
#include <vector>
#include <cstdlib>

void task() {
    // 尝试分配大量内存,可能导致内存不足
    std::vector<int> v(1000000000); // 尝试分配 4 GB 的内存
}

int main() {
    try {
        // 尝试启动一个异步任务
        auto future = std::async(std::launch::async, task);

        // 等待异步任务的完成并获取结果
        future.get();
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }

    return 0;
}

//输出内容:
Caught exception:bad allocation

该示例中,task 函数尝试分配大量内存。如果系统内存不足,std::vector 的构造函数将抛出 std::bad_alloc 异常。由于这个异常没有在 task 函数内部被捕获,因此异常会传播到 std::async 调用处,进而抛出 std::system_error 异常。

【示例2】劫持 new 让 new 内存不够抛异常

代码语言:javascript
复制
#include <iostream>
#include <future>

void* operator new(size_t size) {
    std::cout << "Overloaded new called with size: " << size << std::endl;
    // 模拟内存不足的情况,分配失败
    throw std::bad_alloc();
}

int main() {
    try {
        // 调用std::async,启动一个异步任务
        auto future = std::async(std::launch::async, [](){
            // 在这个异步任务中进行一些内存分配操作
            int* ptr = new int[100000000]; // 尝试分配非常大的内存
            delete[] ptr;
        });
        
        // 获取异步任务的结果
        future.get();
    } catch(const std::bad_alloc& e) {
        // 捕获异常并输出错误信息
        std::cerr << "Caught bad_alloc exception: " << e.what() << std::endl;
    }

    return 0;
}

//输出内容:
Overloaded new called with size: 176
Caught bad_alloc exception:bad allocation

该示例中,重载 new 运算符,使其抛出 std::bad_alloc 异常,而不是实际分配内存。在 task 函数内部,尝试分配大量内存,并捕获了 std::bad_alloc 异常。由于 new 运算符的劫持,内存分配失败时会抛出异常,这个异常会在 std::async 调用处被捕获。


🌊2.3 std::async 如果系统线程不够有没有可能异常

这种情况下,std::async 可能会抛出 std::system_error 异常。

在使用 std::async 时,如果系统线程不够,可能会导致无法启动新线程而引发异常【这通常不是由于内存不足引起的,而是由于达到了系统对同时运行线程数量的限制】

【示例】系统线程不够抛异常

代码语言:javascript
复制
#include <iostream>
#include <future>
#include <vector>
#include <thread>
#include <chrono>

void task() {
    // 模拟一个耗时的任务
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Task executed in thread: " << std::this_thread::get_id() << std::endl;
}

int main() {
    try {
        std::vector<std::future<void>> futures;
        
        // 启动多个异步任务
        for (int i = 0; i < 10000000000000; ++i) {
            futures.push_back(std::async(std::launch::async, task));
        }
        
        // 等待所有异步任务完成
        for (auto& future : futures) {
            future.get();
        }
    } catch(const std::system_error& e) {
        // 捕获系统错误异常并输出错误信息
        std::cerr << "Caught system_error exception: " << e.what() << std::endl;
    }

    return 0;
}

该示例启动了多个异步任务,每个任务执行一个模拟的耗时操作。如果系统没有足够的线程资源来启动这些线程,会抛出 std::system_error 异常。


🌞3. gdb调试async详情

需求:使用gdb直接调到 async 内部调用 linux api,然后直接改返回值来模拟【创建线程,async里每个new和linux调用,测试每个调用失败会怎样】


🌊3.1 模拟调用 new 失败场景

【示例】设计思路:使用 std::async 启动一个异步任务,并在异步任务中调用了 new 函数使其失败。

注意:GDB不能直接设置让 new 失败,因为它的行为是动态的,而不是由GDB控制。所以这里重载了new并且使用全局变量simulate_allocation_failure控制调用new是否能够成功。

  • simulate_allocation_failure=false:调用new成功【不涉及构造函数及复杂对象,否则调用的就不是malloc了】
  • simulate_allocation_failure=true:调用new失败
代码语言:javascript
复制
#include <iostream>
#include <future>
#include <unistd.h>

bool simulate_allocation_failure = true; // 设置为 true 来模拟分配失败

void* operator new(size_t size) throw() {
    if (simulate_allocation_failure) {
        std::cout << "Simulating new failure for size " << size << std::endl;
        throw std::bad_alloc(); // 抛出异常
    } else {
        std::cout << "Custom new called with size: " << size << std::endl;
        return malloc(size);
    }
}

void operator delete(void* ptr) noexcept {
    std::cout << "Custom delete called" << std::endl;
    free(ptr);
}

void task() {
    // 在异步任务中调用 new 函数
    std::cout << "Using new to create ptr..." << std::endl;
    int* ptr = new int(42);
    std::cout << "ptr's value is: " << *ptr << std::endl;
    delete ptr;

    // 在异步任务中调用 Linux API
    std::cout << "Calling Linux API getpid()..." << std::endl;
    pid_t pid = getpid();
    std::cout << "Process ID: " << pid << std::endl;
}

int main() {
    try {
        // 启动异步任务
        auto future = std::async(std::launch::async, task);

        // 等待异步任务完成
        future.get();
    } catch(const std::exception& e) {
        std::cerr << "Caught exception in main: " << e.what() << std::endl;
    }

    return 0;
}

运行输出:

gdb调试说明在new失败时【重载new】会直接被main函数的catch捕获。

分析如下: 代码中,异常是由 operator new 函数抛出的。 operator new 中,当 simulate_allocation_failure 被设置为 true,意味着模拟分配失败时,使用 throw std::bad_alloc(); 语句来抛出 std::bad_alloc 异常。该异常由异步任务中的 std::async 函数捕获,并将其传播到 main 函数中。 std::async 函数创建一个异步任务,并返回一个 std::future 对象,用于获取异步任务的结果。如果异步任务抛出异常,则 std::future::get 函数会在调用时抛出相同的异常。这就是为什么在 main 函数中的 try-catch 块中可以捕获到 std::bad_alloc 异常。

补充说明:

std::async 为什么会调用多次new? 发现原因:将simulate_allocation_failure=false 设置为false时【说明new在不涉及构造函数时会成功】结果如下:

原因在于std::async 内部用到了智能指针shared_ptr,会调用new并且后期会自动调用delete。


🌊3.2 模拟调用 linux api 失败场景

【示例】设计思路:使用 std::async 启动一个异步任务,并在异步任务中调用了 linux api 使其失败。

代码语言:javascript
复制
#include <iostream>
#include <future>
#include <unistd.h>
#include <sys/syscall.h>
#include <stdexcept>

void task() {
    // 在异步任务中调用 Linux API
    std::cout << "Calling Linux API nonexistent_syscall()..." << std::endl;
    if (syscall(-1) == -1) { // 调用不存在的系统调用,会返回 -1 表示失败
        throw std::runtime_error("Failed to call Linux API"); // 抛出异常
    }
}

int main() {
    try {
        // 启动异步任务
        auto future = std::async(std::launch::async, task);

        // 等待异步任务完成
        future.get();
    } catch(const std::exception& e) {
        std::cerr << "Caught exception in main: " << e.what() << std::endl;
    }

    return 0;
}

运行输出:

gdb调试说明在 throw 抛出异常时会直接被main函数的catch捕获。

分析如下: 代码中,异常是由 std::future::get() 函数捕获的。 在 main() 函数中,异步任务通过 std::async(std::launch::async, task) 启动,这里返回一个 std::future 对象。然后调用 future.get() 等待异步任务完成,并获取其结果。如果异步任务中抛出了异常,future.get() 函数会在主线程中抛出相同的异常。因此,在 main() 函数中的 try-catch 块中捕获了这个异常。 在异步任务中,调用了一个不存在的系统调用 nonexistent_syscall(),它返回了 -1 表示失败。在 task() 函数中,当系统调用失败时,抛出了一个 std::runtime_error 异常。这个异常被 future.get() 函数捕获,并传播到了主线程中,最终被 main() 函数的 try-catch 块捕获。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-05-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 🌞1. std::async 简介
  • 🌞2. 问题梳理
    • 🌊2.1 std::async(异步执行) 到 future get 直接调用会如何抛异常
      • 🌊2.2 std::async 如果通过劫持让 new 内存不够,有没有可能抛异常
        • 🌊2.3 std::async 如果系统线程不够有没有可能异常
        • 🌞3. gdb调试async详情
          • 🌊3.1 模拟调用 new 失败场景
            • 🌊3.2 模拟调用 linux api 失败场景
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档