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

Time:09-05

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));
    }
}

CodePudding user response:

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

CodePudding user response:

You're already reinitialize (move new one into) thread_sync_read in SerialPort::read_sync, everything should works.


at destructor, you need to remember delete read_sync

SerialPort::~SerialPort(){
  close_port(); // if necessary to close port
  delete thread_sync_read;
}

or if you declare thread_sync_read not as (raw) pointer

class SerialPort{
public:
    std::jthread thread_sync_read;
}

then you don't need to delete it.

SerialPort::~SerialPort(){
  close_port(); // if necessary
}

note that the destructor of std::jthread would perform necessary request_stop() and join() by itself.

  • Related