Search code examples
c++stdthread

Create new instance of std::thread/std::jthread on every read call


I am developing a serial port program using boost::asio.
In synchronous mode I create a thread every time read_sync function is called. All reading related operation are carried in this thread (implementation is in read_sync_impl function).
On close_port or stop_read function reading operation is stopped.
This stopped reading operation can be restarted by calling the read_sync function again.
read_sync function will never be called successively without calling close_port or stop_read function in between.

I wish to know how to implement a class wide std::jthread along with proper destructor when I call my read_sync function. In languages like Kotlin or Dart the garbage-collector takes care of this. What is C++ implementation of this.

bool SerialPort::read_sync(std::uint32_t read_length, std::int32_t read_timeout)
{
    this->thread_sync_read = std::jthread(&SerialPort::read_sync_impl, this);
    return true;
}

bool SerialPort::read_sync_impl(const std::stop_token& st)
{
    while(true)
    {
        ...
        if (st.stop_requested())
        {
            PLOG_INFO << "Stop Requested. Exiting thread.";
            break;
        }
    }
}

bool SerialPort::close_port(void)
{
    this->thread_sync_read->request_stop();
    this->thread_sync_read->join();
    this->port.close();
    return this->port.is_open();
}

class SerialPort
{
public :
    std::jthread *thread_sync_read = nullptr;
    ...
}

Actual Code

bool SerialPort::read_sync(std::uint32_t read_length, std::int32_t read_timeout)
{
    try
    {
        if (read_timeout not_eq ignore_read_timeout)
            this->read_timeout = read_timeout;//If read_timeout is not set to ignore_read_timeout, update the read_timeout else use old read_timeout
        if (this->thread_sync_read.joinable())
            return false; // Thread is already running
        thread_sync_read = std::jthread(&SerialPort::read_sync_impl, this);
        return true;
    }
    catch (const std::exception& ex)
    {
        PLOG_ERROR << ex.what();
        return false;
    }
}

void SerialPort::read_sync_impl(const std::stop_token& st)
{
    try
    {
        while (true)
        {
            if (st.stop_requested())
            {
                PLOG_INFO << "Stop Requested in SerialPort::read_sync_impl. Exiting thread.";
                break;
            }
        }
    }
    catch (const std::exception& ex)
    {
        PLOG_ERROR << ex.what();
    }
}

class SerialPort
{
    std::jthread                    thread_sync_read;
    SerialPort() : io(), port(io), thread_sync_read()
    {
        read_buffer.fill(std::byte(0));
        write_buffer.fill(std::byte(0));
    }
}

Solution

  • You don't need to deal with the jthread's destructor. A thread object constructed without constructor arguments (default constructor), or one that has been joined, is in an empty state. This can act as a stand-in for your nullptr.

    class SerialPort
    {
    public :
        std::jthread thread_sync_read;
        ...
    
        SerialPort(...)
        : thread_sync_read() // no explicit constructor call needed, just for show
        {}
        SerialPort(SerialPort&&) = delete; // see side notes below
        SerialPort& operator=(SerialPort&&) = delete;
    
        ~SerialPort()
        {
            if(thread_sync_read.joinable())
                close_port();
        }
        bool read_sync(std::uint32_t read_length, std::int32_t read_timeout)
        {
            if(thread_sync_read.joinable())
                return false; // already reading
            /* start via lambda to work around parameter resolution
             * issues when using member function pointer
             */
            thread_sync_read = std::jthread(
              [this](const std::stop_token& st) mutable {
                return read_sync_impl(st);
              }
            );
            return true;
        }
        bool close_port()
        {
             thread_sync_read.request_stop();
             thread_sync_read.join(); // after this will be back in empty state
             port.close();
             return port.is_open();
        }
    };
    

    Side notes

    • Starting and stopping threads is rather expensive. Normally you would want to keep a single worker thread alive and feed it new read/write requests via a work queue or something like that. But there is nothing wrong with using a simpler design like yours, especially when starting and stopping are rare operations
    • In the code above I delete the move constructor and assignment operator. The reason is that the thread captures the this pointer. Moving the SerialPort while the thread runs would lead to it accessing a dangling pointer