#c #multithreading #qt #qmutex
#c #многопоточность #qt #qmutex
Вопрос:
Просматривая устаревший код старого и большого проекта, я обнаружил, что там использовался какой-то странный метод создания потокобезопасной очереди, что-то вроде этого:
template < typename _Msg>
class WaitQue: public QWaitCondition
{
public:
typedef _Msg DataType;
void wakeOne(const DataTypeamp; msg)
{
QMutexLocker lock_(amp;mx);
que.push(msg);
QWaitCondition::wakeOne();
}
void wait(DataTypeamp; msg)
{
/// wait if empty.
{
QMutex wx; // WHAT?
QMutexLocker cvlock_(amp;wx);
if (que.empty())
QWaitCondition::wait(amp;wx);
}
{
QMutexLocker _wlock(amp;mx);
msg = que.front();
que.pop();
}
}
unsigned long size() {
QMutexLocker lock_(amp;mx);
return que.size();
}
private:
std::queue<DataType> que;
QMutex mx;
};
wakeOne
используется в потоках как своего рода функция «публикации»» и wait
вызывается из других потоков и ожидает неопределенно долго, пока сообщение не появится в очереди. В некоторых случаях роли между потоками меняются местами на разных этапах и с использованием отдельных очередей.
Является ли это вообще законным способом использования QMutex
путем создания локальной переменной? Я вроде понимаю, почему кто-то мог это сделать, чтобы избежать взаимоблокировки при чтении размера que
, но как это вообще работает? Есть ли более простой и идиоматичный способ добиться такого поведения?
Комментарии:
1.
QMutex
Не является локальной —QMutexLocker
являются s, и это совсем другое дело.2. @PaulSanders
wx
ЯВЛЯЕТСЯ локальным? Это тоже заставило меня споткнуться, я думал, что они блокируют один и тот же мьютекс, прежде чем я присмотрелся поближе. И, судя по истории, первая ревизия в репозитории была такой, но сmx
, тогда это было «исправлено», потому что это вызывало взаимоблокировки3. Ах да, извините, пропустил это. Это действительно выглядит странно (и сломано).
4. Я видел подобный устаревший код: человек, написавший этот код, не знает, как работает мьютекс.
5. @DmitryKuzminov смотрит на название Надеюсь, мы говорим не об одном проекте
Ответ №1:
Наличие локальной переменной условия законно. Но обычно это не имеет смысла.
Как вы уже поняли, в данном случае это неправильно. Вы должны использовать член:
void wait(DataTypeamp; msg)
{
QMutexLocker cvlock_(amp;mx);
while (que.empty())
QWaitCondition::wait(amp;mx);
msg = que.front();
que.pop();
}
Обратите также внимание, что вы должны иметь while
вместо if
вокруг вызова QWaitCondition::wait
. Это связано со сложными причинами (возможного) ложного пробуждения — документы Qt здесь неясны. Но что более важно, тот факт, что пробуждение и последующее повторное получение мьютекса не является атомарной операцией, означает, что вы должны перепроверить очередь переменных на предмет пустоты. Это мог быть последний случай, когда вы ранее получали взаимоблокировки / UB.
Рассмотрим сценарий пустой очереди и вызывающего объекта (поток 1) для wait
ввода QWaitCondition::wait
. Этот поток блокируется. Затем появляется поток 2, который добавляет элемент в очередь и вызывает wakeOne
. Поток 1 пробуждается и пытается повторно запросить мьютекс. Однако поток 3 появляется в вашей реализации wait, принимает мьютекс перед потоком 1, видит, что очередь не пуста, обрабатывает единственный элемент и движется дальше, освобождая мьютекс. Затем поток 1, который был разбужен, наконец, получает мьютекс, возвращается из QWaitCondition::wait
и пытается обработать … пустую очередь. Ого.
Комментарии:
1. да, что приводит к UB, потому что они получали указатель на .. что-то.. как
_Msg
или постоянно заканчивающийся дополнительным элементом, оставленным в очереди (до «исправления»). Также похоже, что это должно очистить очередь в сценарии, в котором они ее использовали, потому что после некоторых проверок я выяснил, что они никогда не использовали ту же очередь для «обратной связи», насколько я могу судить