LINUX.ORG.RU

Отладка ошибки многопоточности

 ,


0

4

Столкнулся с некоторой проблемой в своём проекте. Имеются сервисные функции засыпания-пробуждения нитей:

void Thread::resume() {
	pthread_mutex_lock(&m_sleepMutex);
	m_disableSleep++;
	pthread_mutex_unlock(&m_sleepMutex);
	pthread_cond_broadcast(&m_sleepCond);
}
		
static void Thread::sleep() {
	Thread* thread = current();
	pthread_mutex_lock(&(thread->m_sleepMutex));
	while (thread->m_disableSleep <= 0) {
		pthread_cond_wait(&(thread->m_sleepCond), &(thread->m_sleepMutex));
	}
	thread->m_disableSleep--;
	pthread_mutex_unlock(&(thread->m_sleepMutex));
}

А также некоторая логика:

Поток 1 взводит некую переменную-флаг, делает работу, а затем делает sleep.
Поток 2 в какой-то момент времени (но точно после установки флага, ибо в нём есть в некотором роде его проверка) сбрасывает флаг, а затем делает resume для потока 1.
Поток 1 просыпается и проверяет, почему его разбудили, проверяя флаг. Если флаг сброшен, то продолжает работу сначала, иначе выходит из цикла, ибо это показатель ошибки, если поток разбудили, а флаг не сбросили.

Проблема в том, что с некоторой небольшой вероятностью происходит состояние ошибки (поток 1 проснулся и увидел, что флаг не сбросили), хотя её быть не должно. При этом единственный код, который вызывает resume таки перед этим безусловно сбрасывает флаг (в настоящий момент других вызовов resume банально нет). Более того, если поставить точку останова на выход из цикла при обнаружении взведённого флага, то отладчик видит, что флаг сброшен.

Я также сделал проверку - передавал в resume булева-параметр (значение по умолчанию сделал false), который сохранял в классе потока. В коде, который сбрасывает флаг, передавал инвертированное текущее значение флага после сброса (то есть true). А в потоке 1 считывал поле класса потока в локальную переменную (а затем присваиваю полю класса false), а затем в новую локальную переменную - текущее состояние флага. В отладчике ставлю точку остановка на команду сразу после создания этих локальных переменных и получаю, что оба флага равны true, хотя такого быть не может (ибо другой поток присваивает полю класса значение обратное флагу).

Флаги описаны как voltatile bool, проект собирается с -O0, но это не помогает. При использовании условных точек остановка баг полностью пропадает (вероятно, многопоточность становится не такой многопоточной, если отладчик постоянно останавливает приложение и проверяет условие). Также можно просто забить на флаг и работать дальше вместо выхода из цикла - всё будет идеально работать. Но это не решение, потому что в будущем не сброшенный флаг будет свидетельствовать об ошибке. Пытался создать минимально работающую демонстрацию бага, но не получилось.

Как вообще такое отлаживать? Такое возможно, чтобы, например, это было следствием работы программы на многоядерной системе - значение флага закешировалось у одного ядра и новое дошло до него не сразу? Я пробовал добавлять __sync_synchronize между сбросом флага и вызовом resume, а также между sleep и проверкой флага, но это не помогло.

★★★★★
Ответ на: комментарий от b0r3d0m

Нет он про то, что происходящее в соседнем треде для текущего треда с точки зрения конпелятора такая же НЁХ как если бы некоторое значение менялось аппаратно. Просто он пример хуевый привел.

К реордерингу volatile не имеет отношения от слова «совсем», потому что реордеринг делается железом и нельзя гарантировать порядок выполнения просто написав инструкции в определенной последовательности.

anonymous
()
Ответ на: комментарий от anonymous

Ну, например, потому, что flag больше не используется в этой функции, компилятор в таких случаях еще может выдать что-то вроде «the variable flag set but not used», если результат не используется в этом потоке, то и операцию делть необязательно.

anonymous
()
Ответ на: комментарий от anonymous

Ну это с локальной переменной так можно, а с нелокальной-то с какой стати?

anonymous
()
Ответ на: комментарий от anonymous

К реордерингу volatile не имеет отношения от слова «совсем»

volatile из стандарта C++? Да, не имеет. А вот volatile некоторых реализаций вполне себе имеет.

потому что реордеринг делается железом

Реордеринг может происходить как при компиляции, так и при выполнении. При компиляции — компилятором, при выполнении — процессором.

и нельзя гарантировать порядок выполнения просто написав инструкции в определенной последовательности

Можно гарантировать, добавив ключевое слово volatile в некоторых реализациях на некоторых платформах.

Например, у MS — https://msdn.microsoft.com/en-us/library/12a04hfd.aspx

b0r3d0m
()
Ответ на: комментарий от b0r3d0m

можно много чего делать, есть разнообразные барьеры для обращения к памяти и примитивы компилятора для их использования. но это уже не уровень среднего программирования. там надо понимать, что делаешь. вообще, их довольно редко используют и обычно когда что-то меняется снаружи, независимо от программы.

Iron_Bug ★★★★★
()
Ответ на: комментарий от b0r3d0m

Это у тебя надо спросить. Ты чего добивался-то? Блеснуть «сакральным» знанием хотел? Не получилось, все про этот костыль и без тебя знают.

anonymous
()
Ответ на: комментарий от b0r3d0m

При компиляции — компилятором

И volatile от этого все равно не спасет.

anonymous
()
Ответ на: комментарий от anonymous

Согласен, пример не тот, вот правильные: адын, два.

Да, глобальную не выкенет, но циклы соптимизирует до неузнаваемости легко.

Ифы, вроде тоже может, за счет кеширования в регистры...

shkolnick-kun ★★★★★
()
Последнее исправление: shkolnick-kun (всего исправлений: 1)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.