两种处理方法
在C++中,如果你在一个通过std::thread
创建的线程中抛出了一个异常,并且没有在该线程中捕获它,那么当线程结束时,异常会被传递到包含线程。但是,直接从std::thread
对象中获取这个异常并不是直截了当的。
从C++17开始,你可以使用std::thread::join()
来等待线程完成,并且如果线程中有未捕获的异常,则会传播到调用join()
的代码中。这意味着如果你的线程抛出了一个异常并且你尝试调用join()
,那么这个异常会在主线程中被捕获。
以下是一个简单的例子:
#include <iostream>
#include <thread>
#include <exception>
void throwExceptionInThread()
{
throw std::runtime_error("Exception from thread");
}
int main()
{
std::thread t(throwExceptionInThread);
try {
t.join(); // 如果线程中有异常抛出,这里会传播出来
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << '\n';
}
return 0;
}
在这个例子中,如果throwExceptionInThread
函数抛出一个异常,那么这个异常会在main
函数中被捕获并处理。
对于C++11和C++14,你也可以这样做,但是如果没有显式地捕获异常,那么这个异常就会丢失,因为std::thread
对象一旦销毁,异常信息就会消失。因此,在这些标准版本中,确保你的线程逻辑总是捕获任何可能抛出的异常,或者确保你在std::thread
对象销毁之前捕获这些异常。
另外一种方法是手动管理异常,比如通过使用共享状态来传递异常信息:
#include <iostream>
#include <thread>
#include <exception>
#include <shared_ptr>
class ExceptionStorage {
public:
void setException(std::exception_ptr ep) { m_exception = ep; }
std::exception_ptr getException() { return m_exception; }
private:
std::exception_ptr m_exception;
};
void throwExceptionInThread(std::shared_ptr<ExceptionStorage> storage)
{
try {
// 模拟线程工作
throw std::runtime_error("Exception from thread");
} catch (...) {
// 将异常存储在共享状态中
storage->setException(std::current_exception());
}
}
int main()
{
auto storage = std::make_shared<ExceptionStorage>();
std::thread t(throwExceptionInThread, storage);
t.join(); // 等待线程结束
std::exception_ptr ep = storage->getException();
if (ep) {
try {
std::rethrow_exception(ep);
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << '\n';
}
}
return 0;
}
这种方法可以确保即使在线程结束之后,异常仍然可以被捕获和处理。
案例展示
让我们来看一个具体的示例,其中我们将在一个线程中执行一些任务,并处理可能发生的异常。我们将创建一个类来存储异常,并将此类的实例作为参数传递给线程。这样,如果线程中的操作抛出异常,我们可以将异常保存在共享状态中,并在主线程中重新抛出。
假设我们需要在一个独立的线程中执行一些文件处理任务,比如读取文件并将内容转换为大写。为了演示异常处理机制,我们故意让程序在某些条件下抛出异常。
以下是完整的示例代码:
#include <iostream>
#include <thread>
#include <exception>
#include <memory>
#include <fstream>
#include <string>
// 用于在线程间传递异常的类
class ExceptionStorage {
public:
void setException(std::exception_ptr ep) { m_exception = ep; }
std::exception_ptr getException() const { return m_exception; }
private:
std::exception_ptr m_exception;
};
// 在线程中运行的任务
void processFile(const std::string& filename, std::shared_ptr<ExceptionStorage> storage)
{
std::ifstream file(filename);
if (!file.is_open()) {
// 模拟文件打开失败的情况
storage->setException(std::current_exception());
return;
}
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
// 转换内容为大写
for (char& c : content) {
c = std::toupper(c);
}
// 假设这里输出转换后的内容
std::cout << content << std::endl;
}
int main()
{
auto storage = std::make_shared<ExceptionStorage>();
std::thread t(processFile, "nonexistentfile.txt", storage);
// 等待线程结束
t.join();
std::exception_ptr ep = storage->getException();
if (ep) {
try {
std::rethrow_exception(ep);
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << '\n';
}
}
return 0;
}
在这个示例中,我们试图打开一个名为 nonexistentfile.txt
的文件,但实际上这个文件不存在。因此,当我们尝试打开文件时,std::ifstream
构造函数会抛出一个 std::runtime_error
异常(或类似异常)。这个异常被捕获,并通过 std::current_exception()
函数捕获当前的异常指针,然后将其存储在 ExceptionStorage
对象中。
当主线程调用 t.join()
并等待子线程完成后,它可以检查 ExceptionStorage
对象中的异常指针。如果存在异常指针,它会重新抛出异常,从而允许主线程处理这个异常。如果文件存在并且可以成功读取,那么不会有任何异常被存储,程序将正常运行。
案例扩展
我们可以在上面的基础上进一步扩展,以确保我们的代码更加健壮,并处理各种可能的情况。例如,我们可以添加更多的错误处理逻辑,以及提供更详细的错误信息。此外,还可以改进代码结构,使其更具模块化和可重用性。
下面是一个更详细的示例,包括错误处理和一些额外的功能:
- 增强错误处理:不仅处理文件不存在的情况,还处理其他潜在的错误,如权限问题等。
- 日志记录:增加日志记录功能,以便更好地跟踪错误和调试信息。
- 分离功能:将文件处理逻辑分离到一个单独的函数中,以提高代码的可读性和可维护性。
下面是改进后的代码:
#include <iostream>
#include <thread>
#include <exception>
#include <memory>
#include <fstream>
#include <string>
#include <sstream>
// 用于在线程间传递异常的类
class ExceptionStorage {
public:
void setException(std::exception_ptr ep) { m_exception = ep; }
std::exception_ptr getException() const { return m_exception; }
private:
std::exception_ptr m_exception;
};
// 用于记录日志的辅助函数
void logError(const std::string& message) {
std::cerr << "[ERROR] " << message << std::endl;
}
// 文件处理逻辑
void convertFileToUppercase(const std::string& filename, std::shared_ptr<ExceptionStorage> storage)
{
std::ifstream file(filename);
if (!file.is_open()) {
// 捕获文件打开失败的情况
try {
std::ifstream file(filename); // 重新尝试一次,以确保捕获异常
} catch (const std::exception& e) {
logError("Failed to open file: " + filename);
storage->setException(std::current_exception());
return;
}
}
std::stringstream buffer;
buffer << file.rdbuf();
std::string content = buffer.str();
// 转换内容为大写
for (char& c : content) {
c = std::toupper(c);
}
// 输出转换后的内容
std::cout << "Converted content: " << content << std::endl;
}
int main()
{
auto storage = std::make_shared<ExceptionStorage>();
std::thread t(convertFileToUppercase, "nonexistentfile.txt", storage);
// 等待线程结束
t.join();
std::exception_ptr ep = storage->getException();
if (ep) {
try {
std::rethrow_exception(ep);
} catch (const std::exception& e) {
logError("Caught exception: " + std::string(e.what()));
}
}
return 0;
}
说明
- 日志记录:添加了一个
logError
辅助函数,用于记录错误信息。 - 文件处理逻辑分离:将文件处理逻辑封装在
convertFileToUppercase
函数中,使得主函数更加简洁。 - 增强错误处理:在尝试打开文件时,增加了对异常的捕捉,并记录详细的错误信息。
通过这样的改进,代码变得更加清晰和易于维护。同时,通过日志记录,我们可以更容易地诊断问题所在。
————————————————
最后我们放松一下眼睛