вызов destroy на дескрипторе сопрограммы вызывает segfault

Недавно я начал экспериментировать с сопрограммами C ++, используя gcc-10.

Приведенный ниже код выполняется точно так же, как и предполагалось, до основных выходов, которые разрушают экземпляр task, вызывая отказ оператора _coro.destroy();.

Почему это segfault?

#include <iostream>
#include <coroutine>

class task
{
public:
  struct promise_type;
  using handle = std::coroutine_handle<promise_type>;
  struct promise_type
  {
    std::suspend_never initial_suspend() const noexcept { return {}; }
    std::suspend_never final_suspend() const noexcept { return {}; }
    auto get_return_object() noexcept { return task{handle::from_promise(*this)}; }
    void unhandled_exception() { std::terminate(); }
  };

  ~task() { if (_coro != nullptr) { std::cout << "Destroying task\n"; _coro.destroy(); } }

  task(task const &) = delete; // Not implemented for brevity...
  task &operator=(task &&other) = delete;
  task(task &&rhs) = delete;

private:
  handle _coro = nullptr;
  task(handle coro) : _coro{coro} { std::cout << "Created task\n"; }
};

std::coroutine_handle<> resume_handle;

struct pause : std::suspend_always
{
  void await_suspend(std::coroutine_handle<> h)
  {
    resume_handle = h;
  }
};

task go()
{
  co_await pause();

  std::cout << "Finished firing\n";
}

int main(int argc, char *argv[])
{
  auto g = go();

  resume_handle();

  return 0;
}

person James Peach    schedule 19.07.2020    source источник
comment
Поскольку final_suspend вашего обещания suspend_never, вы говорите "Разрешить сопрограмме самоуничтожиться по завершении". И затем вы вручную уничтожаете в ~task деструкторе, что является ошибкой двойного освобождения.   -  person Raymond Chen    schedule 19.07.2020
comment
Похоже, это решение этой проблемы. Спасибо.   -  person James Peach    schedule 19.07.2020


Ответы (1)


Несколько вещей:

Рекомендуется использовать suspend_always или вернуть coroutine_handle для возобновления с final_suspend, а затем вручную .destroy() дескриптор в деструкторе.

~my_task() {
    if (handle) handle.destroy();
}

Это связано с тем, что возвращение suspend_never может привести к разрушению дескриптора для вас, поскольку Раймонд Чен указал - в противном случае вам нужно было вручную отслеживать, где дескриптор может быть активен или может быть уничтожен.


В моем случае segfault произошел из-за двойного освобождения, что может произойти, если дескриптор был скопирован где-нибудь еще. Помните, что coroutine_handle<> - это не владеющий дескриптор состояния сопрограммы, поэтому несколько копий могут указывать на одну и ту же (потенциально освобожденную) память.

Предполагая, что вы не выполняете никакого ручного вызова деструктора, убедитесь, что в любой момент времени используется только одна копия вашего дескриптора.

Помните, что семантика копирования будет использоваться вместо семантики перемещения во многих случаях, поэтому вам необходимо удалить конструктор копирования и операторы присваивания.

Вы также должны определить конструктор перемещения, поскольку (согласно cppreference, так что возьмите с толку) конструктор перемещения на coroutine_handle<> является неявно объявленным, что означает, что внутренний указатель просто скопировано и не установлено в nullptr (дополнительная информация представлена ​​в этом ответе).

Следовательно, конструктор перемещения по умолчанию приведет к созданию двух полностью действительных (таким образом bool(handle) == true) coroutine_handle<> объектов, в результате чего несколько деструкторов сопрограмм будут пытаться .destroy() один экземпляр сопрограммы, вызывая двойное освобождение и, следовательно, потенциальный сегментарный сбой.

class my_task {
    coroutine_handle<> handle;
public:
    inline my_task(my_task &&o) : handle(o.handle) {
        o.handle = nullptr; // IMPORTANT!
    }

    ~my_task() {
        if (handle) handle.destroy();
    }

    my_task(const my_task &) = delete;
    my_task & operator =(const my_task &) = delete;
};

Обратите внимание, что Valgrind является предпочтительным инструментом для отладки таких ошибок. На момент написания этой статьи Valgrind, представленный в репозиториях Apt, немного устарел и не поддерживает системные вызовы io_uring, поэтому он захлебнется из-за ошибки. Клонируйте последнюю версию мастера и выполните сборку / установку, чтобы получить исправления для io_uring.

person Qix - MONICA WAS MISTREATED    schedule 31.10.2020