[DSSP]libdssp 健壮优化:异常错误处理与防御性的优化

发布者: 站长-R 分类: C++/C,IT技术交流,生物信息 发布时间: 2026-04-16 16:37 访问量: 100 次浏览

在生物信息学领域,DSSP 是计算蛋白质二级结构的黄金标准工具。libdssp 作为其 C++ 库实现,允许开发者在自己的程序中嵌入 DSSP 算法。然而在实际集成过程中,我发现该库在异常处理和线程安全方面存在一些隐患,特别是在处理非标准输入或非法参数时容易导致程序崩溃。我在构造函数中传递了错误的参数,导致DSSP出现程序崩溃,为了使得库具有更强的鲁棒性,我们其实可以优化一下代码的防御性,防止用户提供错误参数时,出现程序崩溃的现象。

问题现象

在我的 C++ 项目中集成 libdssp 后,使用以下代码调用 DSSP:

dssp dsspObj(db, 1, 4, true);

当处理某些全原子 PDB 文件时,程序在计算 PPII 螺旋阶段弹出“abort() has been called”对话框并崩溃。在这里插入图片描述
调试器捕获的异常指向 std::thread::~thread() 中调用的 std::terminate()。控制台最后输出为:

calculating pp helices
Exception in CalculatePPHelices: Unsupported stretch length

然后我去看调取堆栈的信息,发现异常出现在~thread(),析构函数出现了线程问题,

FlexTrimmer.exe!std::thread::~thread() 行 98 C++
FlexTrimmer.exe!dssp::dssp'::1′::dtor$1() C++
vcruntime140_1d.dll!00007ffdd6dd6ef0() 未知
vcruntime140_1d.dll!00007ffdd6dd40f9() 未知
vcruntime140_1d.dll!00007ffdd6dd65cb() 未知
vcruntime140_1d.dll!00007ffdd6dd2ac5() 未知
vcruntime140_1d.dll!00007ffdd6dd2d4a() 未知
vcruntime140_1d.dll!00007ffdd6dd6d3b() 未知
ntdll.dll!00007ffde9df5d6f() 未知
ntdll.dll!00007ffde9d701b4()

问题根源

1. 参数传递错误

libdssp 的构造函数参数 min_poly_proline_stretch_length 用于指定检测 PPII 螺旋所需的最小连续残基数。查看源码发现,CalculatePPHelices 函数中的 switch 语句仅处理 case 2case 3,其余值均会执行 default 分支并抛出 std::runtime_error("Unsupported stretch length")。我当时传入的 4 正是非法值,触发了异常。

2. 构造函数中的线程安全隐患

异常发生在 dssp::dssp 构造函数中:

dssp::dssp(const cif::datablock &db, int model_nr, int min_poly_proline_stretch, bool calculateSurfaceAccessibility)
    : m_impl(new DSSP_impl(db, model_nr, min_poly_proline_stretch))
{
    if (calculateSurfaceAccessibility)
    {
        std::thread t([this] { m_impl->calculateSurface(); });
        m_impl->calculateSecondaryStructure();   // 这里抛出异常
        t.join();
    }
    else
        m_impl->calculateSecondaryStructure();
}

calculateSecondaryStructure() 抛出异常时,栈展开过程会销毁局部对象 t。由于 tstd::thread 类型,且仍处于 joinable() 状态,其析构函数会调用 std::terminate(),导致程序直接终止。这是典型的异常安全缺陷。

C++ 标准规定,当异常从函数体抛出时,该函数内的所有局部对象会被自动销毁(按构造的相反顺序)。std::thread t 是构造函数中的局部变量,因此会在异常传播出构造函数之前被销毁。

std::thread 的析构函数行为是:

如果线程处于 joinable() 状态(即关联了一个活跃线程且未被 join() 或 detach()),则调用 std::terminate() 终止整个程序。

由于异常在 t.join() 之前抛出,t 仍然关联着后台计算表面可及性的线程。因此,当栈展开销毁 t 时,~thread() 检测到线程可加入,直接触发 std::terminate()进一步引起报错。

优化方案

我们采用多合优化的策略:修复构造函数中的线程管理问题,同时为 PPII 计算增加防御性异常捕获,保证即使发生异常也不会影响主流程。

修改一:加固 dssp 构造函数

将原始构造函数改为异常安全版本:

dssp::dssp(const cif::datablock &db, int model_nr, int min_poly_proline_stretch, bool calculateSurfaceAccessibility)
    : m_impl(new DSSP_impl(db, model_nr, min_poly_proline_stretch))
{
    if (calculateSurfaceAccessibility)
    {
        std::thread t([this] { m_impl->calculateSurface(); });
        try
        {
            m_impl->calculateSecondaryStructure();
        }
        catch (...)
        {
            if (t.joinable())
                t.join();
            throw;   // 重新抛出异常,供上层处理(但至少线程已安全 join)
        }
        if (t.joinable())
            t.join();
    }
    else
    {
        m_impl->calculateSecondaryStructure();
    }
}

修改说明
使用 try-catch 包裹可能抛出异常的主线程计算函数。在 catch 块中确保后台线程被 join() 后再重新抛出异常。这样即使计算失败,线程资源也能正确回收,避免了 std::terminate 的触发。

修改二:为 CalculatePPHelices 添加异常保护

CalculatePPHelices 函数体最外层包裹 try-catch

void CalculatePPHelices(std::vector<residue> &inResidues, statistics &stats, int stretch_length)
{
    try
    {
        if (cif::VERBOSE)
            std::cerr << "calculating pp helices" << std::endl;

        size_t N = inResidues.size();

        const float epsilon = 29;
        const float phi_min = -75 - epsilon;
        const float phi_max = -75 + epsilon;
        const float psi_min = 145 - epsilon;
        const float psi_max = 145 + epsilon;

        std::vector<float> phi(N), psi(N);

        for (uint32_t i = 1; i + 1 < inResidues.size(); ++i)
        {
            phi[i] = static_cast<float>(inResidues[i].mPhi.value_or(360));
            psi[i] = static_cast<float>(inResidues[i].mPsi.value_or(360));
        }

        for (uint32_t i = 1; i + 3 < inResidues.size(); ++i)
        {
            switch (stretch_length)
            {
                case 2:
                {
                    if (phi_min > phi[i + 0] or phi[i + 0] > phi_max or
                        phi_min > phi[i + 1] or phi[i + 1] > phi_max)
                        continue;

                    if (psi_min > psi[i + 0] or psi[i + 0] > psi_max or
                        psi_min > psi[i + 1] or psi[i + 1] > psi_max)
                        continue;

                    switch (inResidues[i].GetHelixFlag(helix_type::pp))
                    {
                        case helix_position_type::None:
                            inResidues[i].SetHelixFlag(helix_type::pp, helix_position_type::Start);
                            break;
                        case helix_position_type::End:
                            inResidues[i].SetHelixFlag(helix_type::pp, helix_position_type::Middle);
                            break;
                        default:
                            break;
                    }

                    inResidues[i + 1].SetHelixFlag(helix_type::pp, helix_position_type::End);

                    if (inResidues[i].GetSecondaryStructure() == structure_type::Loop)
                        inResidues[i].SetSecondaryStructure(structure_type::Helix_PPII);
                    if (inResidues[i + 1].GetSecondaryStructure() == structure_type::Loop)
                        inResidues[i + 1].SetSecondaryStructure(structure_type::Helix_PPII);
                }
                break;

                case 3:
                {
                    if (phi_min > phi[i + 0] or phi[i + 0] > phi_max or
                        phi_min > phi[i + 1] or phi[i + 1] > phi_max or
                        phi_min > phi[i + 2] or phi[i + 2] > phi_max)
                        continue;

                    if (psi_min > psi[i + 0] or psi[i + 0] > psi_max or
                        psi_min > psi[i + 1] or psi[i + 1] > psi_max or
                        psi_min > psi[i + 2] or psi[i + 2] > psi_max)
                        continue;

                    switch (inResidues[i].GetHelixFlag(helix_type::pp))
                    {
                        case helix_position_type::None:
                            inResidues[i].SetHelixFlag(helix_type::pp, helix_position_type::Start);
                            break;
                        case helix_position_type::End:
                            inResidues[i].SetHelixFlag(helix_type::pp, helix_position_type::StartAndEnd);
                            break;
                        default:
                            break;
                    }

                    inResidues[i + 1].SetHelixFlag(helix_type::pp, helix_position_type::Middle);
                    inResidues[i + 2].SetHelixFlag(helix_type::pp, helix_position_type::End);

                    if (inResidues[i + 0].GetSecondaryStructure() == structure_type::Loop)
                        inResidues[i + 0].SetSecondaryStructure(structure_type::Helix_PPII);
                    if (inResidues[i + 1].GetSecondaryStructure() == structure_type::Loop)
                        inResidues[i + 1].SetSecondaryStructure(structure_type::Helix_PPII);
                    if (inResidues[i + 2].GetSecondaryStructure() == structure_type::Loop)
                        inResidues[i + 2].SetSecondaryStructure(structure_type::Helix_PPII);
                    break;
                }

                default:
                    throw std::runtime_error("Unsupported stretch length");
            }
        }
    }
    catch (const std::exception& e)
    {
        if (cif::VERBOSE)
            std::cerr << "Exception in CalculatePPHelices: " << e.what() << std::endl;
        // 出错时不更新任何二级结构,保持原有 Loop 状态,程序继续执行
    }
    catch (...)
    {
        if (cif::VERBOSE)
            std::cerr << "Unknown exception in CalculatePPHelices" << std::endl;
        // 同样忽略,保证程序不崩溃
    }
}

修改说明
PPII 检测是 DSSP 的附加功能,其异常不应影响标准 8 类二级结构(H、E、B、G、I、T、S)的输出。捕获所有异常并仅打印错误日志(当 cif::VERBOSE 非零时),函数静默返回,程序继续执行。

鲁棒性测试

若传入错误参数时,只会抛出异常信息,不会出现程序崩溃现象!

calculating pp helices
Exception in CalculatePPHelices: Unsupported stretch length

关键变化:

  • 异常信息 Exception in CalculatePPHelices: Unsupported stretch length 不再出现(参数合法后)。
  • PPII 标记(P)正确分配给符合条件的残基。
  • 程序正常退出,返回码为 0。

即使故意传入非法值(如 4),程序也只会打印异常信息并跳过 PPII 计算,而不会崩溃,展现了良好的容错性。

总结

本次优化解决了 libdssp 集成中的两个关键缺陷:

  1. 构造函数异常不安全:通过加入 try-catch 和安全的线程 join 逻辑,消除了因异常导致的 std::terminate 崩溃。
  2. PPII 计算缺乏防御性:通过捕获内部异常并降级处理,保证了核心功能的稳定性。

修改后的 libdssp 在面对非法参数、数据异常等边缘情况时表现出更强的鲁棒性,且不影响正常功能。该优化方案已实际应用于我的项目中,运行稳定。

    如果觉得本站对您有帮助,请随意赞赏。您的支持将鼓励本站走向更好!!

    发表回复

    您的邮箱地址不会被公开。 必填项已用 * 标注