1

I have been trying to implement a very basic thread-safe Go-like channel class with Boost.ASIO 1.78 and C++20 coroutines. Sending works without any problems until it hits the max_size limit. When the producer goes into the waiting state it won't wake up even after a consumer has triggered _feedback_timer. Why does this happen and how can it be fixed?

The full program:

#include <boost/asio.hpp>
#include <chrono>
#include <iostream>
#include <limits>
#include <queue>
#include <thread>

using namespace boost::asio;

template<typename Type>
class Channel
{
public:
  Channel(any_io_executor executor, std::size_t max_size = std::numeric_limits<std::size_t>::max())
      : _timer{ executor }, _feedback_timer{ executor }
  {
    _max_size = max_size;
    _timer.expires_at(boost::posix_time::pos_infin);
    _feedback_timer.expires_at(boost::posix_time::pos_infin);
  }
  template<typename Other>
  awaitable<void> send(Other&& value)
  {
    std::unique_lock lock{ _mutex };
    while (_values.size() > _max_size) {
      boost::system::error_code ec;
      auto awaitable = _feedback_timer.async_wait(redirect_error(use_awaitable, ec));
      lock.unlock();
      co_await std::move(awaitable);
      lock.lock();
    }

    _values.push(std::forward<Other>(value));
    _timer.expires_at(boost::posix_time::pos_infin);
    co_return;
  }
  awaitable<Type> receive()
  {
    std::unique_lock lock{ _mutex };

    // wait for signal
    while (_values.empty()) {
      boost::system::error_code ec;
      auto awaitable = _timer.async_wait(redirect_error(use_awaitable, ec));
      lock.unlock();
      co_await std::move(awaitable);
      lock.lock();
    }

    // return value
    _feedback_timer.expires_at(boost::posix_time::pos_infin);
    auto value = std::move(_values.front());
    _values.pop();
    co_return value;
  }
  auto get_executor() noexcept
  {
    return _timer.get_executor();
  }

private:
  std::size_t _max_size;
  std::mutex _mutex;
  deadline_timer _timer;
  deadline_timer _feedback_timer;
  std::queue<Type> _values;
};

int main()
{
  io_service service;
  Channel<std::string> channel{ service.get_executor() };

  // producer
  co_spawn(
    service,
    [&]() -> awaitable<void> {
      for (int i = 0; true; ++i) {
        co_await channel.send("result-" + std::to_string(i));
        // std::this_thread::sleep_for(std::chrono::milliseconds{ 1 });
      }
    },
    detached);

  // consumer
  co_spawn(
    service,
    [&]() -> awaitable<void> {
      while (true) {
        auto result = co_await channel.receive();
        std::cout << "Received: " << result << "\n";
      }
    },
    detached);

  // quick and dirty
  auto guard = make_work_guard(service);
  for (int i = 0; i < 16; ++i) {
    std::thread{ [&] { service.run(); } }.detach();
  }
  service.run();
}

I have tried it on Arch Linux with GCC-11 and Clang-13. My CMake looks something like this:

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)

include(FetchContent)
FetchContent_Declare(
  Boost
  URL https://boostorg.jfrog.io/artifactory/main/release/1.78.0/source/boost_1_78_0.tar.gz
  URL_HASH SHA256=94ced8b72956591c4775ae2207a9763d3600b30d9d7446562c552f0a14a63be7
)
FetchContent_MakeAvailable(Boost)
FetchContent_GetProperties(Boost SOURCE_DIR Boost_INCLUDE_DIR)
find_package(Boost 1.78 REQUIRED)

add_executable(channel_test main.cpp)
target_link_libraries(channel_test PRIVATE Boost::boost)
terrakuh
  • 140
  • 7

0 Answers0