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

如何在不使用awaitable的情况下调用协程可移动Caller的函数并避免悬空?

要解决可移动Caller对象移动后协程指针悬空的问题,同时满足无堆分配、协程始终能访问Caller当前实例的约束,以下是两种可行方案:


方案一:协程恢复时主动获取Caller当前地址

核心思路是让协程不存储任何指向Caller的指针/引用,而是在每次恢复执行时,由Caller主动传递自身的当前地址,从根源上避免悬空指针问题。

代码实现

#include <coroutine>

// 自定义协程任务类型,不存储Caller指针
struct Task {
    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}

        // 自定义栈分配(无堆内存),需根据协程实际栈使用调整大小
        static void* allocate_frame(std::size_t size) {
            static thread_local char stack[1024];
            if (size > sizeof(stack)) throw std::bad_alloc();
            return stack;
        }
        static void deallocate_frame(void* ptr) {}
    };
};

// 等待器:用于在协程恢复时接收Caller的当前地址
struct AwaitCaller {
    Caller* caller = nullptr;

    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<>) const noexcept {}
    Caller* await_resume() const noexcept { return caller; }
};

struct Caller {
    void greet() { /* 实现你的业务逻辑 */ }

    // 创建协程,协程不持有Caller的引用/指针
    Task create_coro() {
        return coro_impl();
    }

    // 恢复协程并传递当前Caller地址
    void resume_coro(std::coroutine_handle<Task::promise_type> handle) {
        // 通过等待器传递当前Caller地址
        auto& awaiter = *static_cast<AwaitCaller*>(handle.promise().awaiter_ptr);
        awaiter.caller = this;
        handle.resume();
    }
};

// 协程实现:每次挂起后恢复时获取最新的Caller地址
Task coro_impl() {
    while (true) {
        AwaitCaller awaiter;
        // 挂起等待Caller恢复并传递地址
        Caller* c = co_await awaiter;
        c->greet();
        // 再次挂起
        co_await std::suspend_always{};
    }
}

方案说明

  • 无堆分配:通过自定义promise_type的内存分配逻辑,使用线程局部栈存储协程帧,完全避免堆内存申请。
  • Caller可移动:协程不存储任何Caller的引用或指针,每次恢复时由Caller传递最新地址,移动后不会出现悬空。
  • 始终访问有效实例:每次协程执行时,拿到的都是Caller的当前地址,确保成员函数调用安全。

方案二:Caller维护协程列表,移动时更新指针

如果需要协程主动访问Caller,可以让Caller维护所有它创建的协程列表,在移动时遍历列表更新协程中的Caller指针。

代码实现

#include <coroutine>

struct Task {
    Caller* caller = nullptr;
    Task* next = nullptr;

    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}

        // 栈分配协程帧
        static void* allocate_frame(std::size_t size) {
            static thread_local char stack[1024];
            if (size > sizeof(stack)) throw std::bad_alloc();
            return stack;
        }
        static void deallocate_frame(void* ptr) {}
    };
};

struct Caller {
    Task* first_coro = nullptr;

    void greet() { /* 实现你的业务逻辑 */ }

    // 移动构造:更新所有关联协程的Caller指针
    Caller(Caller&& other) noexcept {
        for (Task* coro = other.first_coro; coro; coro = coro->next) {
            coro->caller = this;
        }
        first_coro = other.first_coro;
        other.first_coro = nullptr;
    }

    // 移动赋值:清理旧协程指针并更新新协程指针
    Caller& operator=(Caller&& other) noexcept {
        if (this != &other) {
            // 标记旧协程的Caller指针为无效
            for (Task* coro = first_coro; coro; coro = coro->next) {
                coro->caller = nullptr;
            }
            // 更新新协程的Caller指针为当前实例
            for (Task* coro = other.first_coro; coro; coro = coro->next) {
                coro->caller = this;
            }
            first_coro = other.first_coro;
            other.first_coro = nullptr;
        }
        return *this;
    }

    // 创建协程并加入列表
    Task create_coro() {
        Task coro;
        coro.caller = this;
        coro.next = first_coro;
        first_coro = &coro;
        return coro;
    }
};

// 协程实现:使用Task中维护的caller指针访问Caller
Task coro(Task& t) {
    while (true) {
        if (t.caller) {
            t.caller->greet();
        }
        co_await std::suspend_always{};
    }
}

方案说明

  • 无堆分配:协程帧使用栈存储,Task对象可栈分配或作为Caller成员,无需堆内存。
  • Caller可移动:移动时遍历协程列表,将所有协程的caller指针更新为当前实例地址,避免悬空。
  • 协程自动访问有效实例:协程通过Task中维护的指针访问Caller,始终指向最新的实例地址。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.03 00:15:52