You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用std::thread恢复coroutine_handle触发段错误,std::jthread正常

C++协程awaiter段错误问题分析与修复

问题描述

为理解awaiter原理,我实现了一个实验性协程awaiter对象。调用协程foo时,在await_suspend方法中启动std::thread执行任务,任务结束时恢复awaiter。尽管我用unique_ptr管理线程生命周期,并在awaiter析构函数中处理线程joinable,但handle.resume仍触发段错误;换成std::jthread则运行正常,找不到问题所在。

原始代码

#include <coroutine>
#include <iostream>
#include <thread>


struct ReturnObject {
    struct promise_type {
        ReturnObject get_return_object() {
            auto handle = std::coroutine_handle<promise_type>::from_promise(*this);
            return ReturnObject{handle};
        }
        std::suspend_never initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        void unhandled_exception() {
        }

        // void return_void() noexcept {
        // }

        void return_value(int val) noexcept {
            this->value = val;
        }

        std::suspend_always yield_value(int val) {
            this->value = val;
            return std::suspend_always{};
        }

        double get_value() const noexcept { return value; }
        void set_value(double val) noexcept { value = val; }

    private:
        double value{3.14};
    };

    std::coroutine_handle<promise_type> h_;

    ReturnObject(std::coroutine_handle<promise_type> h) : h_{h} {
    }

    operator std::coroutine_handle<promise_type>() { return h_; }

    int get_value() const {
        return h_.promise().get_value();
    }

    ~ReturnObject() {
        h_.destroy();
    }
};


void do_work(std::coroutine_handle<ReturnObject::promise_type>& h) {
    std::cout << "do_work\n";
    h.resume();
}

struct SuspendAlways {
    void await_suspend(std::coroutine_handle<ReturnObject::promise_type> h) {
        std::cout << "await suspend\n";
        th = std::make_unique<std::thread>(do_work, std::ref(h)); //(1)
        //std::jthread (&SuspendAlways::do_work, this, std::ref(h)); //(2)
    }

    void await_resume() {
        std::cout << "await_resume\n";
    }

    bool await_ready() const noexcept { return false; }

    ~SuspendAlways() {
        std::cout << "~SuspendAlways\n";
        if (th->joinable()) th->join();//(1)
    }

    std::unique_ptr< std::thread> th;//(1)
};



ReturnObject foo() {
    std::cout << "1. Hello World!\n";
    co_await SuspendAlways{};
    std::cout << "2. Hello World!\n";
}

int main(int argc, char **argv) {
    auto ret = foo();

    using namespace std::chrono_literals;
    std::this_thread::sleep_for(5000ms);
    std::cout << std::boolalpha << ret.h_.done() << std::endl;
}

问题根源

  • 悬垂引用导致未定义行为:在await_suspend方法中,传递给std::thread的是std::ref(h),而h是该方法的局部参数。当await_suspend执行完毕返回后,局部参数h的生命周期结束,但新线程仍持有它的引用。后续线程中调用h.resume()时,访问的是已经销毁的对象,直接触发段错误。
  • std::jthread正常的原因:std::jthread的构造和销毁逻辑会自动管理线程生命周期,且你注释的代码中可能误打了成员函数调用(实际原代码中do_work是全局函数),但更关键的是,若正确使用std::jthread时,你可能无意中避免了悬垂引用的传递,或者std::jthread的同步机制掩盖了问题,但本质还是原代码的引用失效问题。

修复方案

核心思路是避免悬垂引用,将协程句柄存储到SuspendAlways对象中,确保线程执行期间句柄始终有效。

修改后的代码

#include <coroutine>
#include <iostream>
#include <thread>


struct ReturnObject {
    struct promise_type {
        ReturnObject get_return_object() {
            auto handle = std::coroutine_handle<promise_type>::from_promise(*this);
            return ReturnObject{handle};
        }
        std::suspend_never initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        void unhandled_exception() {
        }

        void return_value(int val) noexcept {
            this->value = val;
        }

        std::suspend_always yield_value(int val) {
            this->value = val;
            return std::suspend_always{};
        }

        double get_value() const noexcept { return value; }
        void set_value(double val) noexcept { value = val; }

    private:
        double value{3.14};
    };

    std::coroutine_handle<promise_type> h_;

    ReturnObject(std::coroutine_handle<promise_type> h) : h_{h} {
    }

    operator std::coroutine_handle<promise_type>() { return h_; }

    int get_value() const {
        return h_.promise().get_value();
    }

    ~ReturnObject() {
        h_.destroy();
    }
};


struct SuspendAlways {
    std::coroutine_handle<ReturnObject::promise_type> h_;

    // 改为成员函数,直接访问对象内存储的句柄
    void do_work() {
        std::cout << "do_work\n";
        h_.resume();
    }

    void await_suspend(std::coroutine_handle<ReturnObject::promise_type> h) {
        std::cout << "await suspend\n";
        h_ = h; // 保存协程句柄到对象中
        th = std::make_unique<std::thread>(&SuspendAlways::do_work, this);
    }

    void await_resume() {
        std::cout << "await_resume\n";
    }

    bool await_ready() const noexcept { return false; }

    ~SuspendAlways() {
        std::cout << "~SuspendAlways\n";
        // 先判断指针是否为空,再检查线程是否可join
        if (th && th->joinable()) {
            th->join();
        }
    }

    std::unique_ptr<std::thread> th;
};



ReturnObject foo() {
    std::cout << "1. Hello World!\n";
    co_await SuspendAlways{};
    std::cout << "2. Hello World!\n";
}

int main(int argc, char **argv) {
    auto ret = foo();

    using namespace std::chrono_literals;
    std::this_thread::sleep_for(5000ms);
    std::cout << std::boolalpha << ret.h_.done() << std::endl;
}

关键修改点

  1. 在SuspendAlways中添加成员变量h_,用于存储协程句柄,避免传递局部变量的引用。
  2. 将do_work改为SuspendAlways的成员函数,直接访问对象内的h_,彻底消除悬垂引用风险。
  3. 析构函数中先判断th是否为空,再检查线程是否可join,避免空指针访问。

内容的提问来源于stack exchange,提问作者Cabbas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 09:37:06