chriskohlhoff / asio

Asio C++ Library
http://think-async.com/Asio
4.94k stars 1.22k forks source link

co_spawn has some errors on lambda capturer #1214

Closed HWZen closed 1 year ago

HWZen commented 1 year ago
#include <iostream>
#include <asio/ip/tcp.hpp>
#include <asio/experimental/as_tuple.hpp>
#include <asio/experimental/awaitable_operators.hpp>
#include <asio/detached.hpp>
using std::chrono::steady_clock;
using namespace std::chrono_literals;
inline constexpr auto use_nothrow_awaitable = asio::experimental::as_tuple(asio::use_awaitable);
inline asio::awaitable<void> timeout(steady_clock::duration duration)
{
    asio::steady_timer timer(co_await asio::this_coro::executor);
    timer.expires_after(duration);
    co_await timer.async_wait(use_nothrow_awaitable);
}

namespace mock{
    class Data{
    public:
        Data() = default;
        Data(const Data&) = default;
        Data(Data&& other) noexcept : data(std::move(other.data)){
            std::cout << "debug: " << "Move" << std::endl;
        }
        Data& operator=(const Data&) = default;
        Data& operator=(Data&&) = default;
        ~Data(){
            std::cout << "debug: " << "Desc" << "\n"
                      << "this: " << this << "\n"
                      << "data: " << data << "\n";
            std::cout.flush();

        }
        auto DebugString() const{
            return data;
        }
        void ParseFromArray(const char *d, size_t size){
            this->data = std::string(d, size);
        }
    private:
        std::string data;
    };
}

asio::awaitable<void> foo1(std::string_view cmd){
    mock::Data command;
    command.ParseFromArray(cmd.data(), cmd.size());
    std::cout << "foo1 command addr: " << &command << std::endl;

    asio::co_spawn(co_await asio::this_coro::executor, 
                   [command = std::move(command)]() /*something go wrong here*/mutable->asio::awaitable<void>{
                        co_await timeout(1s); // wait foo1's command to be destroyed
                        std::cout << command.DebugString() << std::endl;
                        std::cout.flush();
        }, asio::detached);
    std::cout << "debug: " << __LINE__ << command.DebugString() << std::endl;
    co_return;

}

int main(){
    asio::io_context ctx;

    co_spawn(ctx, []()->asio::awaitable<void>{
        co_await foo1("Hello World!_______________________");
    }, asio::detached);

    ctx.run();

    return 0;
}

the above code works in Windows MSVC19.33:

(stdout)
foo1 command addr: 00000200B1FE3A28
debug: Move
debug: Move
debug: Move
debug: Desc
this: 000000D951D3EC20
data:
debug: Desc
this: 00000200B1FE3A50
data:
debug: 67
debug: Desc
this: 00000200B1FE3A28
data:
Hello World!_______________________
debug: Desc
this: 00000200B1FE4D60
data: Hello World!_______________________

but not good in Linux gcc 12.2:

(stdout)
foo1 command addr: 0xd9b518
debug: Move
debug: Move
debug: Move
debug: Desc
this: 0x7ffff6856860
data:
debug: Desc
this: 0xd9b538
data:
debug: Desc
this: 0xd9b558
data: Hello World!_______________________
debug: 62
debug: Desc
this: 0xd9b518
data:
P���___________________
debug: Desc
this: 0xd99bb8
data: P���___________________
free(): double free detected in tcache 2

Process finished with exit code 134

We can see that on MSVC and gcc, Data moved three times, but gcc destroyed Data three times before the end of foo1, one more than MSVC, and it was the last destruction that released the correct data. Is this the reason for the error?

Of cource I can use lambda like this:

    asio::co_spawn(co_await asio::this_coro::executor,
                   [](auto command) ->asio::awaitable<void>{
                        co_await timeout(1s); // wait foo1's command to be destroyed
                        std::cout << command.DebugString() << std::endl;
                        std::cout.flush();
        }(std::move(command)), asio::detached);

That is works on both Windows and Linux.

But what about this ? Does it also come as part of the parameters of the lambda function? I don't think it's very elegant.

HWZen commented 1 year ago

By the way, the above code didn't work on both Windows and Linux

    asio::co_spawn(co_await asio::this_coro::executor,
                   [command = std::move(command)]() mutable->asio::awaitable<void>{
                        co_await timeout(1s); // wait foo1's command to be destroyed
                        std::cout << command.DebugString() << std::endl;
                        std::cout.flush();
        }() /*Added a pair of parentheses here */, asio::detached);
Febbe commented 1 year ago

Eager coroutines will destroy lambda captures at the first suspension point.
Lazy ones will do this immediately.

You must use:

 asio::co_spawn(co_await asio::this_coro::executor, 
                   [](auto command) ->asio::awaitable<void> {
                        std::cout << command.DebugString() << std::endl;
                        std::cout.flush();
                        co_return;
        }(std::move(command)), asio::detached);

Note, that I called the lambda with command. For a better explanation, see https://devblogs.microsoft.com/oldnewthing/20211103-00/?p=105870

HWZen commented 1 year ago

Oh! lambda will be destoryed at the first suspension point?How insidious ! Thank you for answering my problom and the blog you shared.