'boost::asio::io_context::stop segfalt in gtest setup and teardown
Using C++17. I am trying to setup a gtest fixture that will create a fresh io_context to run timers on for each test case. My test segfault about 90% of the time. If I debug and step very slowly, I can get it to run all the way through.
I am not sure what's going on here. I've went and created a new thread and new ioservice every run, just to make sure there was no carry over from previous tests. Then I just deleted the test contents entirely to narrow it down. It throws on the stop call.
#include "gtest/gtest.h"
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <iostream>
class TrafficLightTestSuite : public testing::Test
{
public:
protected:
boost::asio::io_context * m_ioContext;
boost::thread * m_ioThread;
void SetUp() override
{
std::cout << "Setup" << std::endl;
m_ioThread = new boost::thread([&]()
{
m_ioContext = new boost::asio::io_context();
std::cout << "IO Service created" << std::endl;
// Keep the io service alive until we are done
boost::asio::io_service::work work(*m_ioContext);
m_ioContext->run();
std::cout << "IO Context run exited" << std::endl;
delete m_ioContext;
m_ioContext = nullptr;
std::cout << "IO Context deleted" << std::endl;
});
}
void TearDown() override
{
std::cout << "Tear down stopping IO Context" << std::endl;
m_ioContext->stop();
m_ioThread->join();
std::cout << "Thread exit" << std::endl;
delete m_ioThread;
}
};
TEST_F(TrafficLightTestSuite, testTimeLapse)
{
std::cout << "Performing test" << std::endl;
}
Output when running:
Testing started at 1:49 PM ...
Setup
Performing test
Tear down stopping IO Context
Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)
Output when stepping through with debugger:
Testing started at 1:55 PM ...
Setup
IO Service created
Performing test
Tear down stopping IO Context
IO Context run exited
IO Context deleted
Thread exit
Process finished with exit code 0
io_context has to be thread safe, how else would you tell it to stop? Anyone spot a problem?
Solution 1:[1]
The problem is that there is no guarantee that anything on a thread is going to execute before the main thread resumes execution. This caused problems where the ioservice was not created and running before the test case executed, where it was assumed that the ioservice would be running.
This can be fixed using a semaphore that can be waiting on, and queuing up a post on that semaphore before calling ioservice::run. That way, as soon as the ioservice runs, the semaphore posts and the test case cannot execute until that occurs.
class TrafficLightTestSuite : public testing::Test
{
public:
protected:
boost::asio::io_context m_ioContext {};
boost::asio::io_service::work m_work {m_ioContext}; // Keeps the ioservice running when nothing is posted
boost::thread m_ioThread {};
boost::interprocess::interprocess_semaphore m_semaphore {0};
void SetUp() override
{
// When the ioservice starts up, the semaphore will notify anyone that was waiting on it to run
m_ioContext.post([&]() { m_semaphore.post(); });
// Run the io service on its own thread, where completion handlers will be called
m_ioThread = boost::thread(boost::bind(&boost::asio::io_service::run, &m_ioContext));
// Test cases should wait on the semaphore to ensure the io service is running before they execute.
// In production environment, you'd probably init the ioservice in some manner of application setup and do a
// similar notification when everything was initialized.
m_semaphore.wait();
}
void TearDown() override
{
m_ioContext.stop();
m_ioThread.join();
}
};
TEST_F(TrafficLightTestSuite, testInitialColor)
{
// This is guaranteed not to execute until the ioservice is running
}
You could also make your own semaphore using a condition variable. If using C++20, there are standard semaphores available,
Solution 2:[2]
m_ioContext->stop();
happens before m_ioContext = new boost::asio::io_context();
. Just move its creation to SetUp
and deletion to the TearDown
. You even do not need pointers.
#include "gtest/gtest.h"
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <iostream>
class TrafficLightTestSuite : public testing::Test
{
public:
protected:
boost::asio::io_context m_ioContext{};
boost::thread m_ioThread{};
void SetUp() override
{
std::cout << "Setup" << std::endl;
std::cout << "IO Service created" << std::endl;
m_ioThread = boost::thread([&]()
{
// Keep the io service alive until we are done
boost::asio::io_service::work work(m_ioContext);
m_ioContext.run();
std::cout << "IO Context run exited" << std::endl;
});
}
void TearDown() override
{
std::cout << "Tear down stopping IO Context" << std::endl;
m_ioContext.stop();
m_ioThread.join();
std::cout << "Thread exit" << std::endl;
std::cout << "IO Context deleted" << std::endl;
}
};
TEST_F(TrafficLightTestSuite, testTimeLapse)
{
std::cout << "Performing test" << std::endl;
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 |