Zohar81
Zohar81

Reputation: 5118

Thread crash during boost::yield inside catch block

I'm currently investigating a crash scenario that is caused by performing a Boost yield inside a C++ catch block. Here's a minimal reproducible example that leads to a crash. Notice the following points :

  1. Multiple threads in a thread_group are needed to reproduce the crash.
  2. 2 coroutine must be running simultaneously.
  3. When yielding outside the catch block, everything works as expected.
  4. Issue reproduced consistently on both Mac/Win platforms (linux wasn't tested).
  5. When debugging the crash, the call stack was pointing on the yield inside the catch block.
  6. I've found this old forum that mentions a similar issue, but the explanation is unclear to me : https://lists.boost.org.cpp.al/boost-bugs/2016/02/44257.php

Here's a standalone reproduction example. Unfortunately I wasn't to make it shorter, but you can simply run it as-is linked to boost version 1.86-1.83

#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/thread.hpp>
#include <iostream>

namespace asio = boost::asio;
using namespace std::chrono_literals;

void TimerCatchYield(asio::steady_timer& timer,
                     const asio::yield_context& yield) {
  boost::system::error_code ec;
  try {
    throw std::runtime_error("");
  } catch (const std::exception&) {
    // async_wait(yield) here - causes crash
    timer.async_wait(yield[ec]);
  }
  // async_wait(yield) here - works as expected
}

void coroutineA(asio::steady_timer& timer, const asio::yield_context& yield) {
  std::cout << "Coroutine A: Starting timer...\n";

  timer.expires_after(500ms);
  TimerCatchYield(timer, yield);
  std::cout << "Coroutine A: 1\n";

  timer.expires_after(500ms);
  TimerCatchYield(timer, yield);
  std::cout << "Coroutine A: 2\n";

  timer.expires_after(500ms);
  TimerCatchYield(timer, yield);
  std::cout << "Coroutine A: 3\n";

  std::cout << "Coroutine A: all timers expired\n";
}

void coroutineB(asio::steady_timer& timer, const asio::yield_context& yield) {
  std::cout << "Coroutine B: Starting timer...\n";
  boost::system::error_code ec;

  timer.expires_after(500ms);
  timer.async_wait(yield[ec]);
  std::cout << "Coroutine B: 1\n";

  timer.expires_after(500ms);
  timer.async_wait(yield[ec]);
  std::cout << "Coroutine B: 2\n";

  timer.expires_after(500ms);
  timer.async_wait(yield[ec]);
  std::cout << "Coroutine B: 3\n";

  std::cout << "Coroutine B: all timers expired\n";
}

int main() {
  asio::io_context io;
  asio::steady_timer timerA(io);
  asio::steady_timer timerB(io);

  auto strand = make_strand(io);

  boost::thread_group threads;

  spawn(strand,
        [&](const asio::yield_context& yield) { coroutineA(timerA, yield); });

  spawn(strand, [&](const asio::yield_context& yield_b) {
    coroutineB(timerB, yield_b);
  });

  for (int i = 0; i < 4; ++i) {
    threads.create_thread([&io]() { io.run(); });
  }

  threads.join_all();

  std::cout << "Main finished\n";
  return 0;
}

Upvotes: 1

Views: 71

Answers (0)

Related Questions