简体   繁体   中英

How to control the execution of a thread with blocking call

Sorry for the title, I couldn't find a one that describes the problem clearly.

Please read the snippet below. There are 2 threads where the first one controls the execution and the second one does the job.

std::atomic<bool> m_running;
Process* process; // A thread-safe process implementation

//thread 1
void stop()
{
    m_running = false;

    if( process->isRunning() )
        process->kill(); // process->join() returns as a result of this call
}

//thread 2
void run()
{
    m_running = true;

    while( m_running )
    {
        do_something();

        process->start();
        process->join(); // Blocking call
    }

    m_running = false;   
}

I've noticed that if the first thread starts execution while the second thread is busy with the do_something() , it will assign false to m_running and check if the process is running or not. Since the second thread is still in the do_something() call, process->isRunning() will return false and the first thread will return without killing the process.

But second thread will start the process as soon as finishing do_something() as opposed to our stop demand.

The first thing that come to my mind is like the following :

//thread 2
void run()
{
    m_running = true;

    while( m_running )
    {
        do_something();

        if( m_running )
        {
            // flagged line ...
            process->start();
        }

        process->join(); // Blocking call
    }

    m_running = false;   
}

But still it is possible that the whole stop() method in thread 1 can start and finish when the second thread is interrupted in flagged line.

So I decided to use a mutex but I'm not sure if everything is OK or not :

//thread 1
void stop()
{
    mutex.lock();
    m_running = false;

    if( process->isRunning() )
        process->kill(); // process->join() returns as a result of this call

    mutex.unlock();
}

//thread 2
void run()
{
    m_running = true;

    while( m_running )
    {
        do_something();

        mutex.lock();
        if( m_running )
        {
            process->start();
        }
        mutex.unlock();

        process->join(); // Blocking call
    }

    m_running = false;   
}

Although a more precise answer needs more details on what isRunning() , kill() , and join() , there's a design flaw that's apparent here.

Namely, the m_running flag's purpose is overloaded.

A flag like m_running typically implements one of two classical design patterns:

  1. It is to report the status of a thread in question: whether it is running, or not.

or:

  1. It is to control whether the thread in question should run, or stop.

The shown code is attempting to conflate the two design patterns together. And they don't mesh.

If the purpose of m_running is to have the thread in question report its current status, then thread 1's stop() has no business setting this flag. m_running should only be updated by the thread in question, and, indeed, your thread's run() sets it to true when it begins, and false before it returns. But then, run() itself has no business checking the value of m_running inside the if statement. Its only purpose is to report the status of the thread. Not to have the thread do anything.

With this design pattern, kill() typically implements some means for terminating the thread's execution, usually by sending a message of some kind to the thread, that tells the thread to wind down. kill() would then monitor and wait until the thread obeys, and sets m_running to false, indicating that it exited. Or, if this is a joinable thread, kill() would join the thread.

Alternatively, m_running 's purpose can be to signal the thread to stop what it's doing. But in this situation, the execution thread's run() has no business updating m_running itself. All a thread typically does in this design pattern, is execute a while(m_running) loop, waiting for the next message. The typical process for stopping the thread involves setting m_running , and then sending some kind of no-op message to the thread -- the message's only purpose would be to wake up the thread, so it can cycle through the loop and see the updated value of m_running .

The shown code is attempting to conflate the two design patterns into a single variable, and it looks to me like that is the fundamental cause of the design problem.

Separate m_running into two discrete variables: one to report the state of the thread (if that's needed), and a second variable that's used as part of a reliable mechanism for an orderly shutdown of the thread.

Think of it in terms of indivisible transactions.

//thread 1
void stop()
{
    m_running = false;

    {
        //This block should happen as a transaction
        std::lock_guard<std::mutex> lck(m_mutex);
        if( process->isRunning() )
            process->kill();
    }
}

//thread 2
void run()
{
    m_running = true;
    while( m_running )
    {
        do_something();
        {
            //Since starting this process depends on m_running 
            //which could change anytime since we tested it last,
            //we test it again, and start, this time as another transaction
            std::lock_guard<std::mutex> lck(m_mutex);
            if(m_running)
                process->start();
        }
        process->join(); // Blocking call
    }  
}

You don't need to bother about protecting m_running since its the default std::atomic<bool> which is sequentially consistent

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM