У меня есть довольно странный пример, поэтому я кратко изложу здесь контекст, и мы, надеюсь, сможем просто притвориться, что это хорошая идея.
Я использую профилировщик, который требует регулярных вызовов своего макроса FRAME(), чтобы он знал, где начинаются и заканчиваются кадры ЦП игры (объект, который создает макрос, основан на RAII/области действия). Я использую волокна для потоковой обработки (основной «поток» также является рабочим потоком), и этот макрос профилирования поддерживает вызов только из потока, не зарегистрированного в профилировщике в качестве рабочего потока волокна. Следовательно, у меня есть это ужасное краткосрочное решение, когда я общаюсь с отдельным потоком только для этого макроса. Цель состоит в том, чтобы получить как можно более точное время построения/уничтожения объекта RAII в этом отдельном потоке, не нарушая синхронизацию вызывающего потока. Но иногда зависает все приложение. Я не понимаю, как это возможно здесь.
Основной «поток» (на самом деле на волокне, но это не имеет значения)/игровой цикл:
FrameProfile frameProfile("Client Update");
while (!bShouldQuit)
{
frameProfile.StartFrame();
/* Do the game client's work for this frame */
frameProfile.EndFrame();
}
И затем этот объект FrameProfile отвечает за запуск отдельного потока и позволит этому потоку войти в область макроса FRAME, когда StartFrame вызывается из вышеприведенного, и этот поток будет спать в этой области до тех пор, пока не будет вызван EndFrame, после чего он будет проснуться и выйти из области видимости, уничтожив объект измерения кадра профайлера и дав нам, надеюсь, точное время кадра.
struct FrameProfile
{
FrameProfile(const char* tag)
{
pthread_ = std::make_unique<std::thread>(
[tag, this](std::atomic_bool& killFlag) {
while (!killFlag)
{
assert(!endThreadFrame.WasSignalled());
startThreadFrame.WaitConsume();
{
assert(!startThreadFrame.WasSignalled());
assert(!endedThreadFrame.WasSignalled());
// Construct the frame-measuring object using this macro
OPTICK_FRAME(tag);
startedThreadFrame.Signal();
endThreadFrame.WaitConsume();
// endThreadFrame has been signalled - we need to exit scope
// to finish measuring ASAP
}
assert(!endThreadFrame.WasSignalled());
endedThreadFrame.Signal();
}
},
std::ref(bKill_)
);
}
~FrameProfile()
{
bKill_ = true;
if (pthread_)
{
if (pthread_->joinable())
{
pthread_->join();
}
}
}
void StartFrame()
{
assert(!startThreadFrame.WasSignalled());
assert(!startedThreadFrame.WasSignalled());
// Tell thread to start measuring the frame
startThreadFrame.Signal();
// Wait for thread to have started frame measurement
startedThreadFrame.WaitConsume();
}
void EndFrame()
{
assert(!endThreadFrame.WasSignalled());
assert(!endedThreadFrame.WasSignalled());
// Tell thread to end frame measurement
endThreadFrame.Signal();
// Wait for thread to have ended frame measurement
endedThreadFrame.WaitConsume();
}
private:
std::unique_ptr<std::thread> pthread_;
std::atomic_bool bKill_ = false;
struct ThreadSignal
{
std::atomic_bool bSignalled;
std::mutex mutex;
std::condition_variable cv;
void Signal()
{
assert(!bSignalled);
{
std::unique_lock<std::mutex> _(mutex);
bSignalled = true;
}
cv.notify_all();
}
bool WasSignalled()
{
return bSignalled;
}
void WaitConsume()
{
std::unique_lock unique(mutex);
cv.wait(unique, [this]() { return bSignalled == true; });
unique.unlock();
bSignalled = false;
}
};
ThreadSignal startThreadFrame;
ThreadSignal endThreadFrame;
ThreadSignal startedThreadFrame;
ThreadSignal endedThreadFrame;
};
Можете ли вы определить, что я делаю неправильно здесь? Или даже гораздо лучшее решение, я был бы открыт для него! Это редко, но иногда зависает — один из объектов «ThreadSignal» будет иметь логическое значение «true», но все равно будет зависать — я думаю, здесь есть редкая проблема с синхронизацией.
Большое спасибо! Рвал на себе волосы.
Это не решает вопрос, но когда вы запускаете поток C++ и не отсоединяете его, к нему можно присоединиться. Так что тест в if (pthread_->joinable()) pthread_->join()
не нужен.
«Но иногда все приложение зависает. Я не понимаю, как это возможно здесь». -- Когда ваше приложение зависает, вы можете использовать отладчик для проверки состояния вашей программы, когда она зависает. В частности, стек вызовов отдельных потоков может содержать полезную информацию. Например, он должен показывать, ожидает ли поток мьютекс/условную переменную.
@AndreasWenzel действительно, это то, что я сделал, чтобы обнаружить one of the 'ThreadSignal' objects will have its bool as 'true', but will still be stuck
. Я просто не знаю, как это технически возможно, если эта двунаправленная сигнализация не позволит потокам рассинхронизироваться - я не думал, что гонка возможна :)
std::unique_lock unique(mutex);
cv.wait(unique, [this]() { return bSignalled == true; });
unique.unlock();
bSignalled = false;
это не верно. Переместите назначение в bSignalled внутри блокировки.
По сути, откажитесь от состояния условия за пределами мьютекса условия. Есть несколько узких способов доказать законность, но прежде чем вы это сделаете, напишите доказательство и задокументируйте его, потому что каждый такой способ, который я видел законным, был удивительно хрупким; следующий человек, который коснется вашего кода, может легко взломать его.
Изменение этого решит вашу проблему, если я не ошибся.
Также на многих платформах
assert(!bSignalled);
{
std::unique_lock<std::mutex> _(mutex);
bSignalled = true;
}
cv.notify_all();
менее эффективен, чем
assert(!bSignalled);
{
std::unique_lock<std::mutex> _(mutex);
bSignalled = true;
cv.notify_all();
}
так как этот случай оптимизирован ОС (она знает связь между cv и мьютексом). Наконец, один потребитель означает:
std::unique_lock<std::mutex> _(mutex);
bSignalled = true;
cv.notify_one();
верно. Поскольку потребители потребляют сигнал, должен проснуться только один.
void Signal()
{
{
std::unique_lock<std::mutex> _(mutex);
bSignalled = true; // 1a
}
// 2a
cv.notify_all(); // 3a
}
void WaitConsume()
{
std::unique_lock unique(mutex);
cv.wait(unique, [this]() { return bSignalled == true; }); // 1b
unique.unlock();
// 2b
bSignalled = false; //3b
}
Альфа потока находится на уровне 2a.
Бета-версия темы находится на уровне 2b.
bSignalled имеет значение true, альфа собирается уведомить всех.
Бета-версия темы достигает 3b. bSignalled теперь имеет значение false.
Альфа-поток достигает 3a. Оповещает всех. Любой, кто заметил уведомление, просыпается и видит bSignalled как false. Сообщение потеряно.
Вероятно, есть и другие сценарии.
Пожалуйста, предоставьте минимальный воспроизводимый пример.