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 и проверкой флага, но это не помогло.

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

Вот эту простыню текста теперь можно будет использовать в качестве ответа на вопрос «а зачем твой недоЭрланг на крестах». Вот именно для этого, чтобы не клепали самодельные лисапеды с трехугольными колесами и не вытрахивали бы из них букашек на каждом шагу... ;)

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

это уже от 20 лет постоянного профессионального программирования в предметной области.

Мы, видимо, видели совсем разных гуру. Поэтому нет смысла спорить дальше.

Разве что хочу сказать, что вам, видимо, очень повезло с предметной областью, раз она за 20 лет не поменялась.

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

Отладочной печатью.

Проблема в том, что она иногда приводит к синхронизации потоков. Скажем, если два потока выводят на экран данные активно. В итоге код может начать работать банально из-за добавления отладочной печати.

KivApple ★★★★★
() автор топика
Ответ на: комментарий от slackwarrior

Это, насколько я помню, мне надо создать объект типа MutexLock, который в конструкторе лочит mutex, а в деструкторе отпускает.

Вопрос вот в чём. Допустим, у меня есть код:

while (...) {
    mutex.lock();
    ...
    mutex.unlock();
    ...
}

Что с ним делать? Если бы не было кода после mutex.unlock, то всё понятно, но он есть (в моём случае это отправка события «запись» и переход в ожидание события «чтение», если ещё есть данные для записи).

Можно переписать так:

while (...) {
    {
        MutexLock lock(mutex);
        ...
    }
    ...
}

Но вроде как блоки кода на ровном месте (без if, while и т. п.) не совсем стандарт. Или я не прав и так делать можно и нужно?

Ну или следует вынести код, требующих блокировки, в отдельную функцию (что-нибудь типа writeChunk и readChunk) и вызывать ещё в цикле?

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

Но вроде как блоки кода на ровном месте (без if, while и т. п.) не совсем стандарт

Кто сказал?

Или я не прав и так делать можно и нужно?

Можно и нужно.

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

Или я не прав и так делать можно и нужно?

Не прав. Можно. Нужно.

Более того, для совсем уж замороченных случаев можно вспомнить о том, что в std::unique_lock есть метод unlock что позволяет, при крайней необходимости, писать вот так:

while(...) {
  std::unique_lock<std::mutex> lock(mutex);
  ...
  lock.unlock();
  ...
}

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

велосипеды на плюсах иногда можно и даже нужно делать. но только опытным и бывалым велосипедистам, если они могут внятно объяснить, зачем именно в данном случае велосипед с треугольными колёсами и почему для этого нужно потратить пару месяцев не дебаг :) собственно, этим и отличаются плюсы от эрлангов: возможностями для кастомизации и тонких извращений.
впрочем, я много раз видела, как начинающие плюсисты с перепугу начинают городить невесть что для решения простых задач. этакую электронную лазерную мухобойку, которая занимает целый машинный зал. но это именно что от неопытности и отчасти из-за того, что в ВУЗах им выедают мозг принципами ООП, и они потом пытаются везде их присунуть, даже там, где даром не надо.

Iron_Bug ★★★★★
()
Последнее исправление: Iron_Bug (всего исправлений: 1)
Ответ на: комментарий от KivApple

не слушай тех кто выше

надо так:

while (...) {
    ProcessLockedData(...);
    ProcessUnlockedData(...);
}
anonymous
()
Ответ на: комментарий от Iron_Bug

Да в плюсах велосипедостроение вообще развито слишком сильно. Тому есть и объективные причины (например, возможность или невозможность использования исключений заставляет писать код сильно по разному и библиотека, которая построена на исключениях не сможет нормально использоваться там, где исключения отключены). Но, все-таки, это не оправдывает того невероятного желания навелосипедить что-нибудь свое, присущее сиплюсплюсникам :)

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

моя предметная область (железо и сети) как раз постоянно меняется. но опыт есть опыт :) я даже могу прогнозировать, куда, скорее всего, пойдёт развитие того или иного направления в разработке или технологии. и иногда почти инстинктивно угадывать правильное решение в нетривиальных случаях. это уже инженерное чутьё, которое не пропьёшь.

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

Сделал так:

class MutexLock final {
	private:
		Mutex& m_mutex;
		
		bool m_success;
		
	public:
		MutexLock(Mutex& mutex): m_mutex(mutex) {
			m_success = mutex.lock();
		}
		
		MutexLock(Mutex& mutex, timeout_t timeout): m_mutex(mutex) {
			m_success = mutex.lock(timeout);
		}

		MutexLock(const MutexLock& that) = delete;
		
		~MutexLock() {
			if (m_success) {
				m_mutex.unlock();
			}
		}
		
		bool success() const {
			return m_success;
		}
};

Если что, это не касается mutex внутри sleep и wakeup, потому что там нужны pthread mutex, а не самодельные (CAS + вышеописанный механизм событий), к тому же я уверен, что там не случится внезапный return, ибо функции очень простые.

Да, я понимаю, что бы логичнее кидать исключение, если не получилось залочить mutex, однако этот код в том числе должен работать на микроконтроллерах, а там с исключениями туго (даже если компилятор умеет, то рантайм нет).

KivApple ★★★★★
() автор топика
Последнее исправление: KivApple (всего исправлений: 2)
Ответ на: комментарий от eao197

так оно. мне, кстати, чаще попадались области, где исключения были строжайше запрещены. но и в исключениях тоже есть свои плюсы, если их применять грамотно и к месту. специфика, только специфика. потому и недостаточно «знать С++». нужно знать какую-то область его применения, иначе это сферическое программирование в вакууме.
в этом, кстати, и состоит некоторая проблема изучения плюсов и начала карьеры для начинающего программиста: пока у него нет опыта и есть только сферическое знание плюсов после ВУЗа, его никуда не берут. а чтобы получить хороший опыт, нужно работать в большом проекте. а таких проектов мало, потому что это длительные и дорогостоящие разработки, которые могут позволить себе только крупные компании. там всё промышленное велосипедостроение и сосредоточено. а все местечковые поделки на коленке не дотягивают даже до 1% сложности настоящего качественного софта на плюсах. и опыт в таких мелких проектах мало что значит. практически ничего не значит, на самом деле. поэтому в плюсах нельзя прокачать скиллы за пару лет.

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

в мелкоконтроллерах вообще всё сильно отличается. там работа завязана на прерывания, а не на основной цикл, как правило. и сама структура программы становится совершенно другая.

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

Каждый день без pthread'a компиляю, прикинь, да?

Не все, как ты, живут в маленьком уютненьком мирке с одной платформой и pthread'ом.

invy ★★★★★
()
Ответ на: как вас ещё на работе держат? от anonymous

И нафига мне создавать копию Mutex при блокировке? У него как бы есть поле bool m_locked. И если у каждого захватившего будет своё поле, то толку от Mutex будет ноль.

А так всё нормально. Описываем Mutex как глобальную переменную или член какого-нибудь класса. А затем при необходимости хватаем, а потом отпускаем.

Я вообще выпилил конструкторы копирования для Mutex, EventSource и EventListener, потому что иначе сломается вся логика их работы.

KivApple ★★★★★
() автор топика
Последнее исправление: KivApple (всего исправлений: 1)
Ответ на: комментарий от Iron_Bug

Я это знаю. Я не могу похвастаться опытом разработки в 10 лет, но с микроконтроллерами уже какое-то время имею дело. Собственно, для использования внутри прерываний (но не только в них), можно указывать timeout = 0. Например, обработчик прерывания USART может таким образом пихать символы в очередь приёма (а если переполнилась, то что поделать, программист неправильно выбрал размер буфера, либо его такое поведение устраивает).

KivApple ★★★★★
() автор топика
Ответ на: комментарий от tailgunner

ну давай, еще раз напиши, клоун.

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

написаны без std::thread (который появился пару лет назад

c++11, а на дворе 2016 год.

и никому никуда не упирается)

Хз, что вам там и куда не упирается.

и ничо, всё работает.

Ясное дело.

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

когда у тебя лет десять-двадцать опыта в разработке

40 см, уже все поняли :)

Боюсь спросить, что у нее 40 см :)

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

Да, все флаги имеют тип volatile bool.

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

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

Да, не спорю. Но лучше способа пока нет. Бегать в отладчике по таким местам ничуть не лучше.

Для каждого потока: свой лог файл с временными отпечатками и id потока. (Т.к. речь шла об отладчике, значит дело имеем не с миллионом потоков.)

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

Так любое дополнительное действие уже может повлиять на результат гонки, если они присутствует.

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

кто не умеет на них писать, тот не умеет многопоточность в принципе. а кто умеет, тот, наверное, знает, что делает.

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

каким образом std::thread может замедлить выполнение программы на хоть сколько-нибудь значительную величину? хочется увидеть конкретный пример.

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

«обнять и плакать» - про код тех, кто не умеет, но делает. корректный почти не встречается. работает только на том, что amd64 со строгой моделью памяти.

dzidzitop ★★
()

У тебя неправильное понимание того, как работает синхронизация потоков с условными переменными.

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

Во-вторых, из-за того, что ты делаешь lock и unlock в sleep у тебя возникает гонка с потерей события resume.

В-третьих, «взводит некую переменную-флаг» и «сбрасывает флаг» должно происходить строго при залоченном мьютексе.

В-четвертых, все что сделал дальше - лютый костыле_велосипед_с_квадратными_колесами.

shkolnick-kun ★★★★★
()
Последнее исправление: shkolnick-kun (всего исправлений: 1)
Ответ на: комментарий от Iron_Bug

Точно такая же там структура, просто вместо семафоров при синхронизации потока и обработчика прерывания используется эрзац в виде:

1.Поток разрешает прерывание, засыпает (например, на семафоре), просыпается (его будят) обрабатывает данные.
2.Обработчик прерывания запрещает свое прерывание, сбрасывает его флаг, готовит данные,будит поток (например семафором).

Это та же самая producer-consumer problem, ничего необычного.

shkolnick-kun ★★★★★
()
Ответ на: комментарий от shkolnick-kun

у тебя кто-то должен генерить прерывания(сигналы, что угодно). в софте этого кого-то просто нет. там должен быть мастер.

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

Ну так и в обычных программах что-то (чего нет в самой программе) генерирует всякие там SIGKILL, для которых есть обработчики, подобные обработчикам прерываний.

Не вижу особой разницы.

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

АААА, в этом смысле? А то я подумал, что человечеству удалось изобрести что-то новое, пока я портировал BuguRTOS на SDCC/STM8...

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

Ну volatile я сам частенько пользуюсь.

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

shkolnick-kun ★★★★★
()
Ответ на: комментарий от shkolnick-kun

На самом деле это все реализация моих собственных примитивов синхронизации поверх pthreads? Зачем? Чтобы был общий код на Linux и на моей RTOS (в которой планировщик умеет только усыплять текущий поток и будить указанный). И все баги всплывали на десктопе, где их удобнее ловить. В текущем варианте события не теряются.

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

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

А если указатель на регистр объявлен как volatile uint32_t *, то компилятор не выкинет операции с ним.

Но в основном такие вещи скрыты от глаз пользователя за макрос-магией.

shkolnick-kun ★★★★★
()
Последнее исправление: shkolnick-kun (всего исправлений: 1)
Ответ на: комментарий от shkolnick-kun

Про порты ввода-вывода могу поверить. Но как это связано с многопоточностью? Я-то упомянул volatile потому, что многие неверно понимают его предназначение (особенно те, кто пришли в C++ из других языков).

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

Это понятно, но реализовывать надо корректно везде.

У тебя же там вроде одинаковый API и для десктопа и для RTOS.

Или ты десктопом пользуешься только для прототипирования API?

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

А с многопоточностью это связано тем, что иногда надо объявлять переменные или поля структур с volatile, чтобы компилятор не соптимизировал порядок доступа к ним.

Барьеры памяти есть не везде, тащемта.

shkolnick-kun ★★★★★
()
Ответ на: комментарий от shkolnick-kun

А с многопоточностью это связано тем, что иногда надо объявлять переменные или поля структур с volatile, чтобы компилятор не соптимизировал порядок доступа к ним

В стандарте это гарантируют?

Барьеры памяти есть не везде, тащемта

И как тогда это работает? Доступ-то всё равно не атомарный.

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

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

Допустим, мне надо синхронизировать конечный автомат, который крутится в главном цикле и конечный автомат, который крутится в обработчике прерывания(в другом потоке), соответственно имеем:

/*где-то в потоке 1*/
flag = true;
/*где-то в потоке 2*/
if (flag){
    flag = 0;
    /*do something*/
}

Если флаг объявить без volatile, то компилятор выкинет присвоение в потоке 1, а потом «соптимизирует» if в потоке 2 исходя из того, что флаг проинициализировали нулём и нигде не меняли...

И всё - программа не будет работать.

shkolnick-kun ★★★★★
()
Ответ на: комментарий от shkolnick-kun

Если флаг объявить без volatile, то компилятор выкинет присвоение в потоке 1

Это, пардон май френч, схуяли?

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

Атомарность гарантирует аппаратура

Так-то можно и int'ы не синхронизировать, а выравнивать по DWORD'ам.

тут интерес в порядке выполнения операций, или даже в отсутствии выкидывания операций компилятором при оптимизации

Вы о том, что обращения к volatile переменным добавляют memory fences на вашей платформе?

b0r3d0m
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.