🌞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 失败场景
std::async
是 C++11 标准库中用于异步执行的函数,会返回一个 std::future
对象,以获取函数的执行结果。可用其在新线程中执行函数,也可以在当前线程中执行。std::async
的函数声明形式通常如下:
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
的声明。它接受三个参数:
policy
: std::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()
被调用时才执行。
std::async
到 std::future::get
直接调用会抛出异常,主要有两种情况:
std::launch::async
策略,并在调用 std::future::get
之前的函数执行抛出了异常,这种情况下会导致 std::future::get
抛出 std::future_error
异常。【示例1】函数对象抛出异常
#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 策略并在函数执行前抛出异常
#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
异常,说明在获取结果之前已经发生异常。
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】系统内存不足导致异常
#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 内存不够抛异常
#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
调用处被捕获。
这种情况下,std::async
可能会抛出 std::system_error
异常。
在使用 std::async
时,如果系统线程不够,可能会导致无法启动新线程而引发异常【这通常不是由于内存不足引起的,而是由于达到了系统对同时运行线程数量的限制】
【示例】系统线程不够抛异常
#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
异常。
需求:使用gdb直接调到 async 内部调用 linux api,然后直接改返回值来模拟【创建线程,async里每个new和linux调用,测试每个调用失败会怎样】
【示例】设计思路:使用 std::async
启动一个异步任务,并在异步任务中调用了 new 函数使其失败。
注意:GDB不能直接设置让 new 失败,因为它的行为是动态的,而不是由GDB控制。所以这里重载了new并且使用全局变量simulate_allocation_failure控制调用new是否能够成功。
#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。
【示例】设计思路:使用 std::async
启动一个异步任务,并在异步任务中调用了 linux api 使其失败。
#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
块捕获。