简体   繁体   中英

What causes this code `Segmentation fault` after a coroutine call `promise_type::return_value()`?

Updated

The Segmentation fault was caused by trying .get() on an invalid std::future . However the coroutines still don't quite work yet.


[ godbolt ]
[ wandbox ]

I have awaitable type like this :

template<typename T>
struct awaitable
{
  awaitable() = default;
  awaitable(awaitable&& other) : val(std::move(other.val) ) { }
  awaitable(std::future<T>&& other_f) : val(std::move(other_f ) ) { }
  bool await_ready() { return true; }
  void await_suspend(std::experimental::coroutine_handle<> h) { h.resume(); }
  T await_resume() { return val.get(); }
private:
  std::future<T> val;
};

specialize coroutine_traits's promise_type like this

template<typename T, typename ...Args>
struct std::experimental::coroutine_traits<awaitable<T>, Args...>
{
  struct promise_type {
    suspend_never  initial_suspend()      { return {}; }
    suspend_never  final_suspend()        { return {}; }
    void           unhandled_exception()  { std::terminate(); }
    awaitable<T>   get_return_object()    { return std::move(f); }
    T              return_value(T r)      { 
      if constexpr (_DEBUG) std::cout << "About to return_value(" << r << ')' << std::endl;
      return r; }
  private:
    awaitable<T>   f;
  };
};

have some awaiters

awaitable<int> async_add(int a, int b) {
  auto fut = std::async([=]() {
    int c = a + b;
    return c;
  });

  return std::move(fut);
}

awaitable<int> async_fib(int n)
{
  if (n <= 2)
      co_return 1;

  int a = 1, b = 1;

  // iterate computing fib(n)
  for (int i = 0; i < n - 2; ++i)
  {
    int c = co_await async_add(a, b);
    if constexpr (_DEBUG) {
      std::cout << 
        "After co_await async_add(" << std::setw(3) << a << 
        ","    << std::setw(3) << b << ")\t"
        "i = " << i << "\t"
        "c = " << c << "\t"
        "[ fib(" << i + 3 << ") ]" << std::endl;
    }
    a = b;
    b = c;
  }

  co_return b;
}

When I tried to retrieve value :

int main()
{
  std::string str;
  std::getline(std::cin, str);
  awaitable<int> continua_v = async_fib(std::stoi(str) );
  if constexpr (_DEBUG) std::cout << "About to retrieve value..." << std::endl;
  int result = continua_v.await_resume();
  if constexpr (_DEBUG) std::cout << "Retrieving has succeeded : " << result << std::endl;
  return result;

}

Segmentation fault occured

Start

After co_await async_add(  1,  1)   i = 0   c = 2   [ fib(3) ]
After co_await async_add(  1,  2)   i = 1   c = 3   [ fib(4) ]
After co_await async_add(  2,  3)   i = 2   c = 5   [ fib(5) ]
After co_await async_add(  3,  5)   i = 3   c = 8   [ fib(6) ]
After co_await async_add(  5,  8)   i = 4   c = 13  [ fib(7) ]
After co_await async_add(  8, 13)   i = 5   c = 21  [ fib(8) ]
After co_await async_add( 13, 21)   i = 6   c = 34  [ fib(9) ]
After co_await async_add( 21, 34)   i = 7   c = 55  [ fib(10) ]
About to return_value(55)
About to retrieve value...

Segmentation fault

How come it crash when retrieving value?
How do I fix this?

[ godbolt ]
[ wandbox ]


Updated

The Segmentation fault was caused by trying .get() on an invalid std::future . This can be measured by a pre-check.

  T await_resume() { 
    if (val.valid()) {
      if constexpr (_DEBUG) std::cout << "About to val.get()..." << std::endl;
       t_val = val.get();
      if constexpr (_DEBUG) std::cout << "val.get() : " << t_val << std::endl;
    }
    return t_val; }

Unfortunately it seems on the line

awaitable<int> continua_v = async_fib(std::stoi(str) );

the continua_v somehow always gets the default of awaitable<int> which is not desirable.

Start

About to val.get()...
val.get() : 2
After co_await async_add(  1,  1)   i = 0   c = 2   [ fib(3) ]
About to val.get()...
val.get() : 3
After co_await async_add(  1,  2)   i = 1   c = 3   [ fib(4) ]
About to val.get()...
val.get() : 5
After co_await async_add(  2,  3)   i = 2   c = 5   [ fib(5) ]
About to val.get()...
val.get() : 8
After co_await async_add(  3,  5)   i = 3   c = 8   [ fib(6) ]
About to val.get()...
val.get() : 13
After co_await async_add(  5,  8)   i = 4   c = 13  [ fib(7) ]
About to val.get()...
val.get() : 21
After co_await async_add(  8, 13)   i = 5   c = 21  [ fib(8) ]
About to val.get()...
val.get() : 34
After co_await async_add( 13, 21)   i = 6   c = 34  [ fib(9) ]
About to val.get()...
val.get() : 55
After co_await async_add( 21, 34)   i = 7   c = 55  [ fib(10) ]
About to return_value(55)
About to retrieve value...
Retrieving has succeeded : -1

255

[ godbolt ]
[ wandbox ]

I think the lack of interest in this question is mostly because coroutines are so new (not even technically standardized at this point). This is an interesting topic though. I had to do some digging, but I found this talk from CppCon 2016 was immensely helpful, as well as this draft of the coroutine standard .

Getting right into the problem: I think you (and me too prior to doing some research) are a bit confused as to how coroutines are supposed to be written, which unfortunately requires a lot of understanding about how the C++ language implements them under the hood. For instance, coroutines must be controlled (including resumption) via a handle, but you weren't storing the handle anywhere.

Additionally, you should probably not be using std::future or std::async (and just fyi using std::async without std::launch::async is pretty much useless). These types are difficult to use on their own and aren't really conducive to writing coroutines in C++20. For instance, in my testing I found the segfault was caused by the std::future instance not being valid at the point of access.

Here's the fixed code you provided (with unnecessary details stripped away). It has comments for all the important stuff to say what happens where and why. I elected to nest everything directly inside awaitable to be cleaner and easier to read, but it could also be done via coroutine_traits .

Note: I wrote/tested this in VisualStudio 2017 with the latest standard draft and the /await compile flag.

#include <iostream>
#include <utility>
#include <experimental/coroutine>

template<typename T>
class awaitable
{
public: // -- promise type -- //

    // declare the promise type and alias the handle type
    struct promise_type;
    typedef std::experimental::coroutine_handle<promise_type> handle;

    struct promise_type
    {
        T ret_val; // storage location for the return value

        // we need to provide a way to convert a promise into the awaitable object (via the handle)
        awaitable get_return_object() { return awaitable { handle::from_promise(*this) }; }

        // we won't suspend upon starting - we want to start immediately
        auto initial_suspend() { return std::experimental::suspend_never{}; }
        // we need to buffer the return value, so we need to suspend at the end of the coroutine.
        // if we didn't do this the coroutine object could be destroyed and we'd access undefined memory.
        auto final_suspend() { return std::experimental::suspend_always{}; }

        // this is called when the coroutine body uses co_return expr
        // where expr is not cv-qualified void (otherwise return_void() is used).
        // the current coroutine standard draft dictates this MUST return void.
        template<typename U>
        void return_value(U &&v) { ret_val = std::forward<U>(v); }

        // this is called if an exception is thrown in the coroutine.
        // immediately after this, the final suspend is performed (so the coroutine will be done() after this).
        void unhandled_exception()
        {
            // we can either terminate() (i.e. we do not allow/expect exeptions)
            // or we could store and rethrow the exception later (i.e. std::exception_ptr).
            // i'll use the terminate() option since that's what you used.
            std::terminate();
        }
    };

public: // -- non-coroutine support -- //

    // we provide a method to get the value - note that this method is not a coroutine.
    // if we didn't provide this, we couldn't use it in e.g. main() because main() cannot be a coroutine.
    T get()
    {
        while (!co.done()) co.resume(); // resume the coroutine until it's done (at final suspend)
        return co.promise().ret_val;    // then get its value (via copy to allow calling get() multiple times)
    }

public: // -- coroutine support -- //

    bool await_ready() { return co.done(); } // are we already done?
    T    await_resume() { return get(); }    // when await is resumed, we need to block and get the value

    // no special suspend logic - defaults are good.
    // this takes generic coroutine_handle<> rather than handle to be generic.
    // coroutine_handle<> is the same as coroutine_handle<void>.
    // it can kind of be though of like a void* to a coroutine of any type.
    // i.e. we can suspend this coroutine to co_await a coroutine of any type.
    void await_suspend(std::experimental::coroutine_handle<>) {}

public: // -- data -- //

    // we need to store a handle for the coroutine
    handle co = nullptr;

public: // -- ctor / dtor / asgn -- //

    // we can make an awaitable type from a (coroutine) handle
    explicit awaitable(handle h) : co(h) {}

    // in dtor, we need to destroy the coroutine if it's still valid
    ~awaitable() { if (co) co.destroy(); }

    // we can also make an empty awaitable
    awaitable() = default;

    // we can't copy an awaitable
    awaitable(const awaitable&) = delete;
    awaitable &operator=(const awaitable&) = delete;

    // but we can move from one
    awaitable(awaitable &&other) : co(std::exchange(other.co, nullptr)) {}
    awaitable &operator=(awaitable &&other) { co = std::exchange(other.co, nullptr); return *this; }
};

awaitable<int> async_add(int a, int b) { co_return a + b; }

awaitable<int> async_fib(int n)
{
    if (n <= 2) co_return 1;

    int a = 1, b = 1;

    for (int i = 0; i < n - 2; ++i)
    {
        int c = co_await async_add(a, b); // we're a coroutine, so we can co_await the result of async_add
        a = b;
        b = c;
    }

    co_return b;
}

int main()
{
    // we're NOT a coroutine, so we can't suspend and do other things - use .get() rather than co_await
    std::cerr << async_fib(8).get() << '\n';

    return 0;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM