Правильно ли использовать механизм ожидания - уведомления с использованием std::mutex?
Я начал использовать std::mutexes, чтобы остановить поток и ждать, пока другой поток не возобновит его. Это работает так:
Тема 1
// Ensures the mutex will be locked
while(myWaitMutex.try_lock());
// Locks it again to pause this thread
myWaitMutex.lock();
Тема 2
// Executed when thread 1 should resume processing:
myWaitMutex.unlock();
Однако я не уверен, что это правильно и будет работать без проблем на всех платформах. Если это не правильно, как правильно реализовать это в C++11?
4 ответа
Проблемы с кодом
// Ensures the mutex will be locked
while(myWaitMutex.try_lock());
.try_lock()
пытается захватить замок и возвращается true
в случае успеха, т. е. код говорит: " если мы получим блокировку, то попробуйте снова и снова заблокировать ее, пока мы не выйдем из строя ". Мы никогда не сможем "потерпеть неудачу", так как в настоящее время сами владеем блокировкой, на которой мы ждем, и поэтому это будет бесконечный цикл. Кроме того, пытаясь заблокировать с помощью std::mutex
что вызывающий абонент уже установил блокировку на UB, так что это гарантированно UB. Если не удачно, .try_lock()
вернусь false
и while
цикл будет завершен. Другими словами, это не гарантирует, что мьютекс будет заблокирован.
Правильный способ обеспечить блокировку мьютекса - это просто:
myWaitMutex.lock();
Это приведет к блокировке текущего потока (на неопределенный срок) до тех пор, пока он не сможет получить блокировку.
Затем другой поток пытается разблокировать мьютекс, для которого он не заблокирован.
// Executed when thread 1 should resume processing:
myWaitMutex.unlock();
Это не будет работать, так как это UB .unlock()
на std::mutex
что у вас еще нет блокировки.
Использование замков
При использовании мьютекс-блокировок проще использовать объект-оболочку RAII, например std::lock_guard
, Модель использования std::mutex
всегда: " Блокировать -> сделать что-то в критическом разделе -> разблокировать ". std::lock_guard
заблокирует мьютекс в своем конструкторе и разблокирует его в деструкторе. Не нужно беспокоиться о том, когда блокировать и разблокировать и тому подобные вещи низкого уровня.
std::mutex m;
{
std::lock_guard<std::mutex> lk{m};
/* We have the lock until we exit scope. */
} // Here 'lk' is destroyed and will release lock.
Простой замок не может быть лучшим инструментом для работы
Если вы хотите дать сигнал потоку проснуться, то есть структура ожидания и уведомления, использующая std::condition_variable
, std::condition_variable
позволяет любому абоненту отправлять сигнал ожидающим потокам без удержания каких-либо блокировок.
#include <atomic>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
using namespace std::literals;
int main() {
std::mutex m;
std::condition_variable cond;
std::thread t{[&] {
std::cout << "Entering sleep..." << std::endl;
std::unique_lock<std::mutex> lk{m};
cond.wait(lk); // Will block until 'cond' is notified.
std::cout << "Thread is awake!" << std::endl;
}};
std::this_thread::sleep_for(3s);
cond.notify_all(); // Notify all waiting threads.
t.join(); // Remember to join thread before exit.
}
Однако, чтобы еще больше усложнить ситуацию, существует такая вещь, как ложные пробуждения, которые означают, что любые ожидающие потоки могут проснуться в любое время по неизвестным причинам. Это факт в большинстве систем и имеет отношение к внутренней работе планирования потоков. Кроме того, нам, вероятно, нужно проверить, что ожидание действительно необходимо, поскольку мы имеем дело с параллелизмом. Например, если поток уведомлений уведомляет до того, как мы начнем ждать, мы можем ждать бесконечно, если у нас не будет способа сначала проверить это.
Чтобы справиться с этим, нам нужно добавить цикл while и предикат, который сообщает, когда нам нужно ждать и когда мы закончим ждать.
int main() {
std::mutex m;
std::condition_variable cond;
bool done = false; // Flag for indicating when done waiting.
std::thread t{[&] {
std::cout << "Entering sleep..." << std::endl;
std::unique_lock<std::mutex> lk{m};
while (!done) { // Wait inside loop to handle spurious wakeups etc.
cond.wait(lk);
}
std::cout << "Thread is awake!" << std::endl;
}};
std::this_thread::sleep_for(3s);
{ // Aquire lock to avoid data race on 'done'.
std::lock_guard<std::mutex> lk{m};
done = true; // Set 'done' to true before notifying.
}
cond.notify_all();
t.join();
}
Существуют и другие причины, по которым стоит подождать внутри цикла и использовать предикат, такой как "украденные пробуждения", как упоминалось в комментариях @ David Schwartz.
Мне кажется, что вы ищете переменную условия. В конце всегда должен быть способ заставить его работать через мьютексы, но условная переменная является текущим идиоматическим способом C++ для обработки сценария "блок и ожидание, пока что-то произойдет".
Поведение мьютекса, когда поток, который его удерживает, пытается его заблокировать, не определено. Поведение мьютекса, когда поток, который не держит его, пытается разблокировать его, не определено. Так что ваш код может делать что угодно на разных платформах.
Вместо этого используйте мьютекс вместе с условной переменной и логическим предикатом. В псевдокоде:
Блокировать:
Приобретите мьютекс.
Пока предикат равен false, блокируйте переменную условия.
Если вы хотите перезагружать здесь, установите предикат в false.
Отпустите мьютекс.
Выпустить:
Приобретите мьютекс.
Установите для предиката значение true.
Сигнализировать переменную условия.
Отпустите мьютекс.
Перевооружиться:
Приобретите мьютекс.
Установите предикат в ложь.
Отпустите мьютекс.
Пожалуйста, проверьте этот код....
std::mutex m_mutex;
std::condition_variable m_cond_var;
void threadOne(){
std::unique_lock<std::mutex> lck(mtx);
while (!ready){
m_cond_var.wait(lck);
}
m_cond_var.notify_all();
}
void threadTwo(){
std::unique_lock<std::mutex> lck(mtx);
read = true;
m_cond_var.notify_all();
}
Я надеюсь, что вы получите решение. И это очень правильно!!