从final_suspend()调用destroy()会导致崩溃

问题描述 投票:0回答:2

我在

h.destroy()
中调用
final_suspend
以在协程完成执行时自动销毁协程,然后我继续等待协程(等待任务完成)。我发现了一个关于此技术的问题和解释为什么它应该起作用的答案

据我所知,这种技术确实有效,但不适用于调用任务析构函数两次的 MSVC 2022,请参见下面的代码:

#include <coroutine>
#include <optional>

#include <iostream>
#include <thread>

#include <chrono>
#include <queue>
#include <vector>

// simple timers

// stored timer tasks
struct timer_task
{
    std::chrono::steady_clock::time_point target_time;
    std::coroutine_handle<> handle;
};

// comparator
struct timer_task_before_cmp
{
    bool operator()(const timer_task& left, const timer_task& right) const
    {
        return left.target_time > right.target_time;
    }
};

std::priority_queue<timer_task, std::vector<timer_task>, timer_task_before_cmp> timers;

inline void submit_timer_task(std::coroutine_handle<> handle, std::chrono::nanoseconds timeout)
{
    timers.push(timer_task{ std::chrono::steady_clock::now() + timeout, handle });
}

//template <bool owning>
struct UpdatePromise;

//template <bool owning>
struct UpdateTask
{
    // declare promise type
    using promise_type = UpdatePromise;

    UpdateTask(std::coroutine_handle<promise_type> handle) :
        handle(handle)
    {
        std::cout << "UpdateTask constructor." << std::endl;
    }

    UpdateTask(const UpdateTask&) = delete;

    UpdateTask(UpdateTask&& other) : handle(other.handle)
    {
        std::cout << "UpdateTask move constructor." << std::endl;
    }

    UpdateTask& operator = (const UpdateTask&) = delete;

    UpdateTask& operator = (const UpdateTask&& other)
    {
        handle = other.handle;

        std::cout << "UpdateTask move assignment." << std::endl;

        return *this;
    }

    ~UpdateTask()
    {
        std::cout << "UpdateTask destructor." << std::endl;
    }

    std::coroutine_handle<promise_type> handle;
};

struct UpdatePromise
{
    std::coroutine_handle<> awaiting_coroutine;

    UpdateTask get_return_object();

    std::suspend_never initial_suspend()
    {
        return {};
    }

    void unhandled_exception()
    {
        std::terminate();
    }

    auto final_suspend() noexcept
    {
        // if there is a coroutine that is awaiting on this coroutine resume it
        struct transfer_awaitable
        {
            std::coroutine_handle<> awaiting_coroutine;

            // always stop at final suspend
            bool await_ready() noexcept
            {
                return false;
            }

            std::coroutine_handle<> await_suspend(std::coroutine_handle<UpdatePromise> h) noexcept
            {
                // resume awaiting coroutine or if there is no coroutine to resume return special coroutine that do
                // nothing
                std::coroutine_handle<> val = awaiting_coroutine ? awaiting_coroutine : std::noop_coroutine();

                h.destroy();

                return val;
            }

            void await_resume() noexcept {}
        };

        return transfer_awaitable{ awaiting_coroutine };
    }

    void return_void() {}

    // use `co_await std::chrono::seconds{n}` to wait specified amount of time
    auto await_transform(std::chrono::milliseconds d)
    {
        struct timer_awaitable
        {
            std::chrono::milliseconds m_d;

            // always suspend
            bool await_ready()
            {
                return m_d <= std::chrono::milliseconds(0);
            }

            // h is a handler for current coroutine which is suspended
            void await_suspend(std::coroutine_handle<> h)
            {
                // submit suspended coroutine to be resumed after timeout
                submit_timer_task(h, m_d);
            }
            void await_resume() {}
        };

        return timer_awaitable{ d };
    }

    // also we can await other UpdateTask<T>
    auto await_transform(UpdateTask& update_task)
    {
        if (!update_task.handle)
        {
            throw std::runtime_error("coroutine without promise awaited");
        }

        if (update_task.handle.promise().awaiting_coroutine)
        {
            throw std::runtime_error("coroutine already awaited");
        }

        struct task_awaitable
        {
            std::coroutine_handle<UpdatePromise> handle;

            // check if this UpdateTask already has value computed
            bool await_ready()
            {
                return handle.done();
            }

            // h - is a handle to coroutine that calls co_await
            // store coroutine handle to be resumed after computing UpdateTask value
            void await_suspend(std::coroutine_handle<> h)
            {
                handle.promise().awaiting_coroutine = h;
            }

            // when ready return value to a consumer
            auto await_resume()
            {
            }
        };

        return task_awaitable{ update_task.handle };
    }
};

inline UpdateTask UpdatePromise::get_return_object()
{
    return { std::coroutine_handle<UpdatePromise>::from_promise(*this) };
}

// timer loop
void loop()
{
    while (!timers.empty())
    {
        auto& timer = timers.top();
        // if it is time to run a coroutine
        if (timer.target_time < std::chrono::steady_clock::now())
        {
            auto handle = timer.handle;
            timers.pop();
            handle.resume();
        }
        else
        {
            std::this_thread::sleep_until(timer.target_time);
        }
    }
}

// example

using namespace std::chrono_literals;

UpdateTask TestTimerAwait()
{
    using namespace std::chrono_literals;

    std::cout << "testTimerAwait started." << std::endl;

    co_await 1s;

    std::cout << "testTimerAwait finished." << std::endl;
}

UpdateTask TestNestedTimerAwait()
{
    using namespace std::chrono_literals;

    std::cout << "testNestedTimerAwait started." << std::endl;

    auto task = TestTimerAwait();

    co_await 2s;

    //co_await task;

    std::cout << "testNestedTimerAwait finished." << std::endl;
}

// main can't be a coroutine and usually need some sort of looper (io_service or timer loop in this example)
int main()
{
    auto task = TestNestedTimerAwait();

    // execute deferred coroutines
    loop();
}

MSVC 2022 的输出是:

UpdateTask constructor.
testNestedTimerAwait started.
UpdateTask constructor.
testTimerAwait started.
testTimerAwait finished.
testNestedTimerAwait finished.
UpdateTask destructor.
UpdateTask destructor.
UpdateTask destructor.

但是 GCC 11.1.0 的输出是:

UpdateTask constructor.
testNestedTimerAwait started.
UpdateTask constructor.
testTimerAwait started.
testTimerAwait finished.
testNestedTimerAwait finished.
UpdateTask destructor.
UpdateTask destructor.

如您所见,MSVC 2022 有一个额外的析构函数调用,因此 MSVC 2022 生成的代码的行为是未定义的,它可能会格式化您的硬盘驱动器。

MSVC 2022 版本:适用于 x86 的 Microsoft (R) C/C++ 优化编译器版本 19.30.30709

编辑9:

弄清楚发生了什么。 UpdateTask 的析构函数在 MSVC 2022 中被调用两次,请参阅更新的代码。

编辑10:

来自docs:协程已暂停(其协程状态由局部变量和当前暂停点填充)。 调用awaiter.await_suspend(handle),其中handle是代表当前协程的协程句柄。在该函数内部,可以通过该句柄观察挂起的协程状态,并且 该函数负责安排它在某个执行器上恢复,或者被销毁(作为调度返回错误计数)

c++ c++20 coroutine
2个回答
0
投票

看起来这是一个编译器错误,可能已在适用于 x86 的 Microsoft (R) C/C++ 优化编译器版本 19.31.31106.2 中修复,至少现在输出是:

UpdateTask constructor.
testNestedTimerAwait started.
UpdateTask constructor.
testTimerAwait started.
testTimerAwait finished.
testNestedTimerAwait finished.
UpdateTask destructor.
UpdateTask destructor.

0
投票

我同意这似乎是 MSVC 中的一个错误。

我认为有一个解决方法。这个想法是,在

final_suspend::await_ready()
中,如果您想调用
true
,则返回
destroy()
。 i如果返回
true
,则不会调用
await_suspend()
,并且执行会从
final_suspend
末尾开始执行。当发生这种情况时,编译器将添加对
destroy()
的调用。请参阅 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4849.pdf

中的第 9.5.5、5.7、11 节

您可能需要稍微重写一下逻辑,但似乎可行。

© www.soinside.com 2019 - 2024. All rights reserved.