Is it mandatory to lock mutex before signaling on condition variable?

Viewed 614

We have implemented TaskRunner whose functions will be called by different threads to start, stop and post tasks. TaskRunner will internally create a thread and if the queue is not empty, it will pop the task from queue and executes it. Start() will check if the thread is running. If not creates a new thread. Stop() will join the thread. The code is as below.

bool TaskRunnerImpl::PostTask(Task* task) {
  tasks_queue_.push_back(task);
  return true;
}

void TaskRunnerImpl::Start() {
  std::lock_guard<std::mutex> lock(is_running_mutex_);
  if(is_running_) {
    return;
  }
  is_running_ = true;

  runner_thread_ = std::thread(&TaskRunnerImpl::Run, this);
}

void TaskRunnerImpl::Run() {
  while(is_running_) {
    if(tasks_queue_.empty()) {
      continue;
    }
    Task* task_to_run = tasks_queue_.front();
    task_to_run->Run();
    tasks_queue_.pop_front();

    delete task_to_run;
  }
}

void TaskRunnerImpl::Stop() {
  std::lock_guard<std::mutex> lock(is_running_mutex_);
  is_running_ = false;
  if(runner_thread_.joinable()) {
    runner_thread_.join();
  }
}

We want to use conditional variables now otherwise the thread will be continuously checking whether the task queue is empty or not. We implemented as below.

  • Thread function (Run()) will wait on condition variable.
  • PostTask() will signal if some one posts a task.
  • Stop() will signal if some one calls stop.

Code is as below.

bool TaskRunnerImpl::PostTask(Task* task) {
  std::lock_guard<std::mutex> taskGuard(m_task_mutex);
  tasks_queue_.push_back(task);
  m_task_cond_var.notify_one();
  return true;
}

void TaskRunnerImpl::Start() {
  std::lock_guard<std::mutex> lock(is_running_mutex_);
  if(is_running_) {
    return;
  }
  is_running_ = true;

  runner_thread_ = std::thread(&TaskRunnerImpl::Run, this);
}

void TaskRunnerImpl::Run() {
    while(is_running_) {
        Task* task_to_run = nullptr;

        {
            std::unique_lock<std::mutex> mlock(m_task_mutex);
            m_task_cond_var.wait(mlock, [this]() {
                return !(is_running_ && tasks_queue_.empty());
            });

            if(!is_running_) {
                return;
            }

            if(!tasks_queue_.empty()) {
                task_to_run = tasks_queue_.front();
                task_to_run->Run();
                tasks_queue_.pop_front();
            }
        }

        if(task_to_run)
            delete task_to_run;
    }
}

void TaskRunnerImpl::Stop() {
    std::lock_guard<std::mutex> lock(is_running_mutex_);
    is_running_ = false;
    m_task_cond_var.notify_one();
    if(runner_thread_.joinable()) {
        runner_thread_.join();
    }
}

I have couple of questions as below. Can some one please help me to understand these.

  1. Condition variable m_task_cond_var is linked with mutex m_task_mutex. But Stop() already locks mutex is_running_mutex to gaurd 'is_running_'. Do I need to lock m_task_mutex before signaling? Here I am not convinced why to lock m_task_mutex as we are not protecting any thing related to task queue.

  2. In Thread function(Run()), we are reading is_running_ without locking is_running_mutex. Is this correct?

2 Answers

Do I need to lock m_task_mutex before signaling [In Stop]?

When the predicate being tested in condition_variable::wait method depends on something happening in the signaling thread (which is almost always), then you should obtain the mutex before signaling. Consider the following possibility if you are not holding the m_task_mutex:

  1. The watcher thread (TaskRunnerImpl::Run) wakes up (via spurious wakeup or being notified from elsewhere) and obtains the mutex.
  2. The watcher thread checks its predicate and sees that it is false.
  3. The signaler thread (TaskRunnerImpl::Stop) changes the predicate to return true (by setting is_running_ = false;).
  4. The signaler thread signals the condition variable.
  5. The watcher thread waits to be signaled (bad)
    • the signal has already come and gone
    • the predicate was false, so the watcher begins waiting, possibly indefinitely.

The worst that can happen if you are holding the mutex when you signal is that, the blocked thread (TaskRunnerImpl::Run) wakes up and is immediately blocked when trying to obtain the mutex. This can have some performance implications.


In [TaskRunnerImpl::Run] , we are reading is_running_ without locking is_running_mutex. Is this correct?

In general no. Even if it's of type bool. Because a boolean is typically implemented as a single byte, it's possible that one thread is writing to the byte while you are reading, resulting in a partial read. In practice, however, it's safe. That said, you should obtain the mutex before you read (and then release immediately afterwards).

In fact, it may be preferable to use std::atomic<bool> instead of a bool + mutex combination (or std::atomic_flag if you want to get fancy) which will have the same effect, but be easier to work with.

Do I need to lock m_task_mutex before signaling [In Stop]?

Yes you do. You must change condition under the same mutex and send signal either after the mutex is locked or unlocked after the change. If you do not use the same mutex, or send signal before that mutex is locked you create race condition that std::condition_variable is created to solve.

Logic is this:

Watching thread locks mutex and checks watched condition. If it did not happen it goes to sleep and unlocks the mutex atomically. So signaling thread lock the mutex, change condition and signal. If signalling thread does that before watching one locks the mutex, then watchiong one would see condition happen and would not go to sleep. If it locks before, it would go to sleep and woken when signalling thread raise the signal.

Note: you can signal condition variable before or after mutex is unlocked, both cases is correct but may affect performance. But it is incorrect to signal before locking the mutex.

Condition variable m_task_cond_var is linked with mutex m_task_mutex. But Stop() already locks mutex is_running_mutex to gaurd 'is_running_'. Do I need to lock m_task_mutex before signaling? Here I am not convinced why to lock m_task_mutex as we are not protecting any thing related to task queue.

You overcomlicated your code and made things worse. You should use only one mutex in this case and it would work as intended.

In Thread function(Run()), we are reading is_running_ without locking is_running_mutex. Is this correct?

On x86 hardware it may "work", but from language point of view this is UB.

Related