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

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

Да, все флаги имеют тип volatile bool. Хотя для второго флага можно и без volatile. Вероятно, сказывается то, что к основному флагу имеются обращения из первого потока до засыпания (он же его взводит), а ко второму - только после ошибки.

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

Считаю, что тебе пора идти читать про std::thread, std::condition_variable, std::mutex, std::atomic и т.п.

То что ты написал - не C++ а какое-то говно.

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

Тогда показать весь код. В resume/sleep ошибок не вижу.

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

После вызова resume() другая нить в sleep() может начать работу и не сразу.

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

это не «какое-то говно» это pthread. какое-то говно - это искаробочные варианты для нубов, которые заведомо тормозят и сжирают необъяснимое количество ресурсов. пока копошишься с детскими десктопными приложениями, это ещё может проканать. в серьёзных проектах такое недопустимо. к тому же, 99% (если не больше) плюсовых приложений написаны без std::thread (который появился пару лет назад и никому никуда не упирается) и ничо, всё работает.

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

(если не больше) плюсовых приложений написаны без std::thread

Они называются legacy или не очень-то и плюсовые :)

slackwarrior ★★★★★
()

Из этого описания не понятно, ни в чем именно проблема, ни как она может возникать. Нужно больше информации. И примеры того, как ведется работа с самими флагами до/после обращения к resume/sleep.

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

Ну да :) Нужно каждый раз писать с нуля всю фигню :) Царь не даст соврать :)

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

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

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

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

Iron_Bug ★★★★★
()

Шел 2016 год, но некоторые всё еще писали многопоточные программы на мютексах и условных переменных.

tailgunner ★★★★★
()

Шел 2016 год, но некоторые всё еще писали многопоточные программы на мютексах и условных переменных.

tailgunner ★★★★★
()

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

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

а действительно качественной работы на каждой платформе, с оптимизацией.

Когда есть бюджет под это дело — не проблема :) Когда бюджета нет — нет такого вопроса. «Качество» — вещь относительная. С тех пор как издержки на вылизывание ручками превышают профит заказчика :)

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

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

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

вот такой подход и губит софт. поэтому и имеем говнософт ан-масс и плюсы уже сравнивают с жабкой. ноги понятно откуда растут.

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

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

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

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

На практике это отдельно обсуждается (либо выносится за скобки по умолчанию), и если заказчик «нет» — на нет и суда нет: есть в смете — есть в бюджете, нет в смете — извольте доплатить за :) Лишние ресурсы тратить на это «из любви к превозмоганию искусству» никто не почешется.

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

ноги понятно откуда растут.

Из эффективного менеджмента, откуда ж еще-то :) «Обколются своим аджайлом и MBA» :) Дядьки Брукса на них нет.

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

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

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

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

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

Что ифдефы? Писать ифдевы в каждом месте юзания тредов? Так это такой лютый и нечитаемый говнокод будет... Или оберточки писать с подменной имплементации для каждом платформы? Но в чем тогда отличие от std/boost::thread?

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

Ну и да, можно примеры чем std::thread уступает pthread ну что бы это было реально заметно. А не только в синтетическом тесте который в бесконечном цикле создает over 100500 тредов.

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

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

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

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

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

Это только мне показалось, что нехило так старперским снобизмом пахнуло?

Именно в разработке (т.е. в написании кода, да и в проектировании, отчасти) годы опыта мало что значат. Молодой, но толковый разработчик с 3-мя годами опыта может выдавать код намного лучшего качества, чем середнячок с 20-годами опыта просиживания штанов в большом бодишопе.

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

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

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

Ух ты, навеное первый годный коммент на лоре от тебя за все время. Молодец, растешь.

anonymous
()

Ты абсолютно точно уверен, что функция pthread_cond_wait вызывается до pthread_cond_broadcast?

Флаги описаны как voltatile bool

А это-то тут при чём?

Как вообще такое отлаживать?

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

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

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

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

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

а чем синтетический тест не устраивает?

Тем, что в реальной проге никто в бесконечном треде не гоняет создание, локи/анлоки по 100500 раз. А значит этот «провал» по производительности будет размазан по всему коду и скорее всего вообще заметен не будет. А значит этим можно пожертвовать ради нормальной читаемости и кастомизируемости.

Я не отрицаю, что бывают случаи когда производительность очень критична, но 98% задач таковыми не являются. Поэтому я предпочту читаемость и норм ООП, аду из ифдефов и сишных функций. Если появится задача когда каждая мс на счету, то таки да придется извращаться. Но зачем делать преждевременную оптимизацию?

Если в приложение используется сеть и/или БД, то вряд ли скорость тредов будет узким местом=)

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

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

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

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

Я решил проблему и оказалась она в другом. Проверяемый флаг хранился не просто, а в объекте EventListener (а ещё у EventListener указана нить, в которой он создан). EventListener можно зарегистрировать в EventSource. EventSource::broadcast вызовет resume для всех потоков, которые зарегистрировали EventListener, а также сбросит их флаги m_armed. Список зарегистрированных EventListener очищается.

EventListener::wait сначала сделает sleep, а потом вернёт !m_armed (подразумевается, что нас может разбудить что-нибудь типа таймаута, в этом случае получается, что события мы не дождались).

Ещё у EventListener есть метод reset, который позволяет повторно его зарегистрировать. При уничтожении же EventListener удаляет себя из списка EventSource.

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

А использовались эти примитивы внутри очереди. Создаём EventListener, который привязан к событию чтения. Блокируем очередь на запись и пихаем данные сколько влезет в буфер (я реализовал такой алгоритм, который позволяет одновременно читать и писать). Отпускаем mutex, отправляем событие «запись» (нужно для аналогичной функции чтения, которая может ждать, пока появятся новые данные). Если мы записали не все данные, то вызываем EventListener::wait, иначе выходим из цикла записи.

Вот в этом «иначе выходим» и заключалась ошибка. А что если случился EventSource::broadcast, пока мы писали данные? Нас попытаются разбудить, инкрементировав m_disableSleep, но мы то спать не будем, потому что всё записали и выйдем из функции записи. В следующий раз, когда мы войдём в неё, мы не сможем sleep, увидим, что m_armed равен true и подумаем, что что-то случилось.

Решение:

Добавляем в EventListener поле m_processed. При регистрации в EventSource ставим m_processed = false (а вызывать EventListener::EventListener, EventListener::wait, EventListener::~EventListener и EventListener::reset разрешается только одному потоку, поэтому даже volatile не нужен). Аналогично в случае reset. Внутри wait делаем m_processed = true сразу после sleep. Внутри деструктора EventListener смотрим, что нам вернула функция дерегистрации слушателя (она выполняется в критической секции и мы имеем гарантию, что слушатель не сработает в момент удаления). Если она вернула, что слушателя уже не было в списке (а это значит, что он таки сработал когда-то раньше), то проверяем m_processed (деструктор никак не может быть вызван внутри wait, а значит либо мы уже поспали и сбросили флаг, либо ещё не спали и не собираемся, потому что уничтожаем слушателя). Если он равен false, то делаем sleep (который однако тупо уменьшит счётчик и тут же вернётся).

Теперь всё отлично работает.

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

как-то я на ассемблере(!) делала синхронизацию для одного большого проекта в телекоме.

Ну это уже специфика, не надо ее выдавать за правило для всех и вся. И называть проекты которые не требуют миллион потоков детскими, ну кхм, это как-то непрофессионально...

Dudraug ★★★★★
()

И да, нужно это для моей сиплюсплюснутой ОСРВ для микроконтроллеров. Просто помимо голого железа я поддерживаю и POSIX (всего-то заменить планировщик на обращения к pthreads, тем более что из функций управления потоками мне нужно лишь create, sleep, resume, yield и exit + вместо виртуальных таймеров написать обёртку над POSIX timers, остальной код работает без изменений, в том числе реализация семафоров, очередей и событий). Это как удобно при отладке, так и позволит писать программы для работы со всяким железом на одноплатниках (в Linux же есть i2cdev, spidev и т. д.).

И именно поэтому std::thread не подойдёт. Точнее можно попробовать использовать его как «бекэнд» вместо pthreads, но всё равно POSIX timers на винде не заработают вроде как, а там где они есть, там и pthreads есть (впрочем, pthreads под Windows отлично работает).

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

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

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