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)