Comments 78
Скажите, вы читали книгу Дейтела «Введение в операционные системы»?
Не читал.
Пробежался глазами — книга весьма интересная, почитаю на досуге.
В ней просто описаны все базовые варианты решения проблем с дедлоками. Советую почитать. Хоть книга и из 70х, но с тех пор мало что изменилось в алгоритмике мультипоточных вычислений. (или я про это не в курсе)
UFO landed and left these words here
В данной ситуации придётся разблокировать уже выделенные ресурсы и заблокировать их по новой
Такое далеко не всегда возможно, если дополнительные ресурсы понадобились в середине операции, которая с точки зрения доступа к захваченному ресурсу является транзакцией, накладные расходы зачастую полностью невелируют простоту использования подобной стратегии блокировок.
Ну и потом — почему бы не использовать стандартные вещи, наподобие WaitForMultipleObjects?
Ну, во первых — WaitForMultipleObjects это платформозависимая функция, а паттерн создан с претензией на независимость от реализации, именно по этому всё, что касается непосредственно работы с API вынесено в отдельную стратегию. Далее, основная идея паттерна, собственно, за ради чего он был написан — это блокировка ресурсов, необходимых на данном этапе выполнения единым пакетом, и отслеживание ситуаций, когда это не так. Конечно, можно эту логику реализовать и через WaitForMultipleObjects, но мой вариант кажется несколько проще и определённо удобнее.
Можно сделать простую обертку, которую реализовывать по разному на разных платформах, насколько я помню — в линуксе есть аналог WaitForMultipleObjects.
Можно конечно. Программирование такая штука, когда одну и ту же вещь можно сделать большим числом способов. Только данном случае нужно будет дополнительно создать хэндел для каждого разделяемого ресурса, потом удалить его. А так в целом то-же самое. У меня, правда, есть подозрение, что мой вариант будет работать несколько быстрее, хотя я могу и ошибаться.
Только в вашем случае разделяемые ресурсы разделяются между потоками, а если они не являются локальными для процесса — могут быть проблемы.
Ну и в любом случае — лучше использовать API, предоставляемые системой.
Ну ничего себе затея — одним махом блокировать все ресурсы. Представьте реально большую систему — ресурсов могут быть тысячи. Что, все блокировать, когда нужен доступ только к одному?
Одним махом блокируются не все ресурсы, а только нужные в данный момент для данного потока.
Поток выполнялся и завис. Все, прощайте ресурсы.
Поток А занял ресурсы 1, 2, 3, а поток Б не успел запуститься, потому что А заблокировал доступ к ресурсу 3. Но после выполнения половины действий поток А должен синхронизировать данные с потоком Б. Все, коллизия.
Поток выполнялся и завис. Все, прощайте ресурсы.

В данном случае при любом раскладе программа становится неуправляемой и требуется перезапуск.
Но после выполнения половины действий поток А должен синхронизировать данные с потоком Б

Не совсем понял, что вы имеете ввиду. Есть общие ресурсы, в которые поток А произвёл запись, после чего освободил их. Поток Б, подождал, пока они освободятся и начал свой выполнение.
В данном случае при любом раскладе программа становится неуправляемой и требуется перезапуск.
Боже, ненавижу программы, которые вылетают из за любой мелкой ошибки. Ну нельзя же так сразу перезапускать — а если там работа пользователя, которую он делал последние несколько часов? Да и что мешает откатить состояние? Обычно зависание потока, выполняющего делегированную задачу, вполне можно обработать и вернуть программу в консистентное состояние прозрачно для пользователя.
Зависание одного из потоков — это отнюдь не мелкая ошибка. В общем случае, ты не знаешь, что это за поток, для чего он предназначен, и в каком месте его логики произошло зависание. Кроме того, возможно причина зависания кроется совсем не в этом потоке, а в повреждении каких-то общих данных, некорректной логике других потоков и т.д. Для предотвращения описанной вами ситуации гораздо безопаснее использовать авто сохранение во временную папку, и после перезапуска предложить пользователю восстановить данные.
Зависание потока — это та проблема, которую можно исправить. И вообще — я вполне знаю что это за поток, и для чего он предназначен, вопрос только в проектировании верной архитектуры, которая может это сделать.
В скольких процентах случаев зависание потока вызвано повреждением памяти или чем-то еще экзотическим? Зачастую зависание потока — проблема кода потока, а значит можно и попробовать восстановиться.
Опасная практика.
Вы знаете, что это за поток, но вы не имеете представления, какой ассемблерный код сгенерировал компилятор для вашего потока. Когда вы попытаетесь остановить поток — вы не будете иметь не малейшего понятия, на какой именно ассемблерной инструкции он остановится, и что именно он запишет в общие данные.
Если доступ к общим данным осуществляется при помощи транзакций — я знаю что они будут в консистентном состоянии.
Что вы подразумеваете под обращения осуществляются при помощи транзакций?
Вот есть общий ресурс, который содержит поля «A» и «B», вы физически не сможете изменить их одновременно, то есть возможна ситуация:
1) поле «A» изменено
2) поток завис
3) поток убит
4) поле «B» — не изменено
5) поля находятся в несогласованном состоянии.
Или ещё пример, вы меняете какую-либо строку, и в этот момент поток уничтожается — результат непредсказуем. В лучшем случаи будет падение, в худшем — будут сохранены неверные данные.
Ну это было бы слишком примитивно, делать реализацию подобных транзакций в том же потоке, который запросил изменение.
UFO landed and left these words here
Аргументы? Запускаю метод, принимающий на вход граф и выполняющий какую-либо операцию на нем — в отдельном потоке, поток зависает — мне надо значит из за этого всю программу грохнуть?
Вы тоже машину выкидываете, когда у вас шины спустились?
UFO landed and left these words here
Поток можно убить, освободив при этом все ассоциированные с ним ресурсы.
Если я знаю, что поток не выполняет код, который, будучи прерванным — может оставить в неконсистентном состоянии другие части системы в неизвестных местах — его можно убивать.
Я привел пример задачи, в которой поток можно вполне спойойно убить.
Если есть проблемы, например, с памятью — программа все равно рухнет, но проблемы эти достаточно редки на общем фоне причин зависания, поэтому можно ориентироваться на наиболее распространенные причины.
UFO landed and left these words here
Если поток хотя бы иногда выделяет память динамически, его уже нельзя убивать.

Начнем с того, что это верно далеко не везде.
Ну и я предпочту убить поток и сохранить то, что можно сохранить, чем убить приложение.
UFO landed and left these words here
Об этом я и написал несколькими комментариями выше:
И вообще — я вполне знаю что это за поток, и для чего он предназначен, вопрос только в проектировании верной архитектуры, которая может это сделать.

Если я знаю, что поток не выполняет код, который, будучи прерванным — может оставить в неконсистентном состоянии другие части системы в неизвестных местах — его можно убивать.
Если вы пишите небольшую улиту, над кодом которой будете работать только вы — то, может быть, и так. Но, как правило, над этим кодом будут работать и другие люди, которые могут не знать всех не очевидных тонкостей работы с этим кодом. Более того, сегодня вы пишете этот код, помня про его особенности, но если через год вы решите внести незначительное изменение, то совсем не факт что вы вспомните про это особенность. Как самый очевидный пример — поток нельзя убивать, даже если он просто выделяет динамическую память.
Как самый очевидный пример — поток нельзя убивать, даже если он просто выделяет динамическую память.
Говорю же — это верно не всегда.
По поводу «не знать» и «забыл» — это вообще смешно — документация в помощь. Так можно и про блокировки забыть и дедлоков понаставить, и вообще что угодно забыть можно.
Убивать приложение из за зависания потока можно только в тех программах, работа которых мало чего стоит. Да, в крайнем случае можно потерять кусок кода за последний час работы или перезапустить видео-плеер. Но не надо забывать, что есть еще огромное количество ПО, которое должно быть устойчиво к отказам и в котором нельзя просто так взять и выкинуть все накопленные (даже за последнюю минуту) данные.
Что касается некритичных приложений — повторюсь:
Рассчитанная на подобные отказы архитектура позволит обрабатывать их достаточно легко.
В случаях, если восстановить нельзя:
Ну и я предпочту убить поток и сохранить то, что можно сохранить, чем убить приложение.

Тут вы попали пальцем в небо, я как раз работаю над разработкой системы критичной к отказам. И единственным правильным способом сохранить данные — это периодически сбрасывать их на диск, и восстанавливаться из них. если работа системы не была завершена штатно. Чаще всего, намного лучше потерять данные за какой-то не продолжит промежуток времени, чем рисковать нем, что данные окажутся в результате неверными.
По поводу «не знать» и «забыл»

при грамотном дизайне, стараются избегать не очевидных конструкций, в которых нужно обязательно что-то «знать» и «не забывать»
Что здесь неочевидного? Поток, отмеченный для выполнения в заданном режиме не должен вызывать тото и тото. Это даже статическим анализатором зачастую можно проверить.

И что же за критичная к отказам система, которая выбрасывает данные за последнее время, потому что не была спроектирована, чтобы иметь возможность их сохранить?
UFO landed and left these words here
1) Тогда вообще не надо употреблять слово «завис». Если вам удобнее — назовем это «аномально долгим временем выполнения»
2) Я ни разу не говорил что я пишу на C++
3) Я упоминал, что доступ к чему-либо из таких потоков должен осуществляться через специальные механизмы.

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

Все сводится к тому, что я пишу:
Если я знаю, что поток не выполняет код, который, будучи прерванным — может оставить в неконсистентном состоянии другие части системы в неизвестных местах — его можно убивать.
А вы пишете что это нельзя сделать тривиально, по взмаху волшебной палочки.
Да, я знаю что нельзя сделать это тривиально.
UFO landed and left these words here
Ага, значит авторы любой хоть раз зависнувшей программы — не умеют анализировать код и делают тривиальные ошибки.
Скорее наоборот, вы не привыкли глубоко анализировать код, и не понимаете, что зациклиться, или попасть в дедлок программа может и далеко не тривиальным способом, и без написания явного бесконечного цикла. Если, конечно, писать более-менее сложные программы.
У меня небыло подобных ошибок, но это дает мне повода говорить что эти ошибки «примитивны» и допускающие их люди «не могут анализировать код».
Разрешите откланяться из этого топика, если вы считаете, что пишете идеальный код и не можете допустить какую либо из указанных ошибок — это действительно вызывает только улыбку.
Ну или вы терминатор, но в таком случае я тем более откланяюсь, осознав в сравнении свою ничтожность в умении писать идеальный код.
UFO landed and left these words here
Что за детский сад? Оценка сходимости не влияет на ошибки в программировании.
UFO landed and left these words here
Например, два асинхронных потока производят длительные вычисления в два приема. Например, нужно на первом заходе произвести анализ, потом обменяться данными с соседним потоком, и снова продолжить анализ с учетом обмена данных. Потоку А выделили напарника — поток Б. Но запуститься сможет только один из них, так как Б не может использовать заблокированный ресурс. Но А не сможет произвести обмен данными с Б, так как он никогда не запустится из-за блокировки общих ресурсов, например дискового массива.
Сразу встает вопрос — что делать, если поток блокирует ресурс на длительный период? Ведь нельзя же запрашивать все ресурсы, которые могут понадобиться на этот период.
Ну и более того — что за synh_, который, как я понял, исходя из того, что класс — синглтон — он один для всех потоков? В таком случае это решение вообще не работает.
что делать, если поток блокирует ресурс на длительный период

Не блокировать общие ресурсы на длительное время. Серьёзно, это плохая практика, в не зависимости от того, как вы их затем собираетесь синхронизировать.
что за synh_

synh_ — экземпляр стратегии синхронизации, как правило один объект mutex и один объект condition.
Не блокировать общие ресурсы на длительное время.
Я не знаю как это реализовать — поэтому это плохо?
Есть случаи, когда надо захватить ресурс на все время выполнения задачи, а оно может составлять несколько секунд. Или в вашем сказочном мире все ресурсы лишены состояния?
synh_ — экземпляр стратегии синхронизации
Отлично — что происходит при выполнении метода lock?
Судя по тому, что я тут вижу — при выполнении одного lock и до выполнения unlock ни один другой поток не сможет выполнить lock (засыпая на ожидании unlock'а) — а в этом случае сам этот класс — источник дедлоков.
Есть случаи, когда надо захватить ресурс на все время выполнения задачи, а оно может составлять несколько секунд
Именно для таких ресурсов, захват которых осуществляется на длительное время и предназначен асинхронный lock. Далее, в общем случаи блокировка shared данных на длительное время не есть гуд, так как они на то и общее, что подразумевают использование из разных потоков.
Отлично — что происходит при выполнении метода lock

При выполнении метода lock происходит попытка заблокировать ресурсы, перечисленные в списке, в случае, если не все ресурсы доступны то поток или засыпает, или возвращает false. Кроме того, происходит проверка, не попытался ли этот конкретный поток заблокировать ресурсы, не освободив перед этим предыдущие.
При чем тут shared данные? Ресурсы — это не только данные.
То, что ресурсы shared — это не значит, что их нельзя блокировать надолго, некоторые устройства, захватываемые в монопольном режиме — это тоже ресурсы.
При вызове метода lock у synh_
А заодно — при вызове wait — работают разные объекты синхронизации или один?
Я не говорил, что это нельзя делать, я только сказал, что это не очень хорошая практика. Какой смысл расшаривать ресурс между потоками, если фактически он монопольно используется одним из них?

В моей реализации стратегии синхронизации, в классе содержатся 2 объекта: один omni::mutex и один связанный с ним omni::condition, которые используются всеми потоками. wait — метод у condition, lock — метод у mutex.
Как так не очень хорошая практика? Это не очень хорошая хорошая практика, если применять ее бездумно, как, впрочем, и любая другая вещь — примененная бездумно. А в случаях когда это применять надо — это более чем стандартная практика.

Сверху остался неотвеченный вопрос — почему не использовать стандартные вещи, наподобие WaitForMultipleObjects?
Или ввести сигналы, которые будут разруливать все без блокировки. Поток будет выполняться, находиться в процессе ожидания, спать или быть зомби.
UFO landed and left these words here
UFO landed and left these words here
SynchronizationManager блокируется не на время захвата ресурса, а только в момент выполнения функций lock и unlock. Логика их тривиальна, и выполнение не займёт много времени.
UFO landed and left these words here
Я это и имел ввиду. Логика функции очень проста, и в случае наличия хотя-бы 2-х ресурсов в списке, будет работать быстрее, чем вызов mutex::lock для каждого ресурса отдельно.
UFO landed and left these words here
Да хватит вам со своим «тся», в статье хватает других ошибок, которые более серьезные и касаются непосредственно алгоритма и его реализации, а это «тся» можно и пропустить (даже и не заметил, где там эта ошибка).
Так не надо писать:
SynchronizationManager::getInstance().lock( lock_list+0, lock_list+3 );

Хотя бы так:
SynchronizationManager::getInstance().lock( lock_list+0, lock_list+ARRAY_SIZE(lock_list) );

где ARRAY_SIZE(x) — или макрос, или template-фукция, считающая количество элементов статического массива.
) я знаю это, написал так для простоты образца, код и так получился достаточно сложным для понимания.
Я когда вижу такие места в коде, начинаю с подозрением относиться к остальному коду этого автора.
Что-то я не понял.

Внутри SynchronizationManager::lock(...) вызывается synh_.wait(), соответствующий ему synh_.signal() находится в SynchronizationManager::unlock(...).

Скажите, а как тогда можно попасть в SynchronizationManager::unlock(...) — там тоже стоит synh_.lock(), который заблокирован предыдущим SynchronizationManager::lock(...), который стоит на synh_.wait()?

Wait освобождает объект синхронизации от блокировки и ожидает выполнения метода Pulse на этом же объекте.
synh — это член (глобольного) SynchronizationManager'а. Соответственно, после начала выполнения функции SynchronizationManager::lock или SynchronizationManager::unlock все другие попытки вызова этих функций из других нитей будут блокироваться до завершения его работы. Это понятно и обычно.

Мне не понятно, что будет происходить, если внутри SynchronizationManager::lock'а сработает строка synh_.wait();

Она может выйти только если сработает synh_.sygnal() в SynchronizationManager::unlock'е, а в него мы попасть не сможем по причинам написанным чуть выше.

Непонятно мне, как оно работать может.
Wait освобождает объект синхронизации от блокировки и ожидает выполнения метода Pulse на этом же объекте.
Проще говоря: smth.wait() это реально:

smth.unlock();
smth.waitPulst();

Но при условии, что эти две строки выполняются атомарно.
Ну тогда приплыли: могут сработать одновременно несколько методов SynchronizationManager::lock(), которые будут модифицировать одну-единственную LockedThreads (через lthread_.insert()), которая на такое использование не рассчитана.
Все вопросы к автору, может у него свои волшебные примитивы синхронизации. Там реализация вообще по сути очень неудачная.
Не совсем понял в чём проблема. Доступ к общим данным происходит в области кода, закрытого mutex-ом.
Далее, VenomBlood немного ошибся, smth.wait() это скорее
smth.unlock();
smth.waitPulst();
smth.lock();
по крайней мере, на такую реализацию рассчитывал, возможно этот момент стоило поподробнее расписать.

Смотрите — ваш smth.unlock() сработал, значит может начать выполнение в другой нити метод SynchronizationManager::lock(), правильно?

Далее, в разных нитях работают два метода SynchronizationManager::lock(), так?

Соответственно, две нити одновременно могут менять LockedThreads, так?

В результате имеем undefined behavior.
Не совсем так. Как я уже писал, smth.wait() — это, по сути, вызов
smth.unlock();
smth.waitPulst();
smth.lock();
То-есть, после сигнала поток проснётся, но не начнёт своё выполнение, пока первый поток не освободит mutex.
Да, упустил строчку, конечно блокировку он дополнительно захватывает в конце.
«до завершения его работы» — имеется в виду работа метода lock/unlock SynchronizationManager'а
В общем, в этом топике очень классная картинка в начале. А всё, что буквами написано — ересь и мракобесие.
Вообще-то, в ситуациях, подобных описанной Вами, когда есть несколько ресурсов, которые используются только совместно — по сути, они являются одним ресурсом, и для него нужен только один объект синхронизации (мютекс, критическая секция, smart lock, неважно).

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

И еще одно замечание: паттерны конечно хорошо, но необходимо пользоваться ими в меру, без фанатизма.
Only those users with full accounts are able to leave comments. Log in, please.