![]() |
+7 (495) 229-0436 | ![]() |
shopadmin@itshop.ru | 119334, г. Москва, ул. Бардина, д. 4, корп. 3 | ![]() |
![]() |
![]() |
|
|
Моя "парадигма" работы с потоками09.11.2012 14:32
MrShoor
Поскольку ошибки, связанные с синхронизацией потоков крайне сложно отлаживать, то самым эффективным способом тут является предупреждение этих самых ошибок. Для этого используются различные парадигмы программирования на разных уровнях абстракции. Нижним уровнем абстракции будем считать работу с объектами синхронизации (критические секции, мьютексы, семафоры). Верхним - такие парадигмы программирования, как Futures and promises, STM (software transactional memory), обмен асинхронными сообщениями и т.п. Верхний уровень абстракции зачастую всегда основан на нижнем. В данной статье я поделюсь своим стилем написания кода на нижнем уровне абстракции. Поскольку я дельфист, то все примеры будут на Delphi, однако все нижесказанное справедливо и для других языков программирования (позволяющих работать с объектами синхронизации конечно) Потокобезопасный объектПервое правило - между потоками работать только с потокобезопасными объектами. Это самое простое, логичное и понятное правило. Однако даже тут есть некоторые особенности. Объект должен быть целиком потокобезопасный, а это значит что все public методы (кроме конструктора и деструктора) нужно синхронизировать. Конструкторы и деструкторы в свою очередь должны быть всегда синхронизированы снаружи объекта. Одна из ошибок на ранних этапах работы с потоками - я забывал о синхронизации конструкторов и деструкторов. И если в делфи с конструктором проблем нет (мы получаем указатель на объект только когда конструктор уже отработал), то с деструктором надо быть внимательным. Синхронизация деструкторов - очень скользкая тема, и я не могу дать каких-либо указаний, как лучше её реализовывать (я не гений многопоточного программирования, а только учусь ;) ). Сам я стараюсь проводить такую синхронизацию через деструктор класса TThread, но это справедливо только для объектов, которые существуют всю жизнь потока.
Блокировки
ОписаниеДругая распространенная проблема - это взаимные блокировки (deadlock-и). Несмотря на то, что это наиболее распространенная проблема, возникающая при синхронизации - тут есть одно не очевидное правило. Если поток единовременно выполняет не больше одной синхронизации, то никаких дедлоков не будет. Здесь под словом синхронизация - я имею ввиду как блокировку ресурса, так и ожидание какого-либо ресурса. Таким образом остановка на мьютексе, закрытие мьютекса, вход в семафор, вход критическую секцию, или отправка сообщения (SendMessage) - это все синхронизации. И в самом деле, если поток А ожидает ресурс, и при этом он не заблокировал ни один ресурс, то его в свою очередь никто не ожидает, а значит взаимной блокировки быть не может.
ПримерыПонимание и строгое выполнение данного условия - ключ к отсутствию deadlock-ов. Давайте рассмотрим на примере, о чем я говорю. Допустим у нас есть некоторый класс:
Следуя тому, что у нас должен быть потокобезопасный объект - я реализовал свойства A и B через геттеры и сеттеры с критической секцией:
Допустим функция DoSomething у нас работает с A и B как-то так:
Эй, но мы ведь используем одну критическую секцию для A и для B, скажет неискушенный писатель. И сразу же "оптимизирует" этот кусок:
И это будет ошибка. Теперь, если мы в обработчике WM_MYMESSAGE попытаемся обратиться к полю A или B - мы получим дедлок. Данный дедлок - очевиден, так как объем кода маленький, данные простые. Но оно становится не тривиальным, когда когда код огромен, появляется куча связей и зависимостей. Согласно правилу - работать только с одной синхронизацией одновременно, вышеописанный код можно "оптимизировать" так:
Поэтому всегда, прежде чем вызвать новую синхронизацию - нужно освободить другие объекты синхронизации. Код в духе:
В большинстве случаев можно считать многопоточным быдлокодом. Я думаю вы уже представляете как надо его переписать:
По данному подходу видно, что нам приходится копировать данные, что может отразиться на производительности. Однако в большинстве случаев объемы данных не велики, и мы можем позволить копировать их.
ДиагностикаНа уровне компилирования такое диагностировать не получится. Однако можно провести диагностику в реалтайме. Для этого нам надо хранить текущий объект синхронизации для каждого потока. Вот пример реализации средства диагностики на Delphi.
Вызываем InitSyncObject когда стартуем новый поток.
В Acquire добавить вызов PushSyncObject(Self), а в Release PopSyncObject. Так же не забываем обрамить в эти функции WaitFor методы у THandleObject. Кроме того, если используем метод TThread.Synchronize то до вызова сохраняем объект TThread, а после извлекаем его (PopSyncObject), если используем API функции SendMessage или WaitFor функции, то до вызова сохраняем хендл (PushSyncObject), после - извлекаем (PopSyncObject).
Плохой кодВ качестве примера плохого кода возьмем… класс TThreadList из модуля Classes.pas
Казалось бы, потокобезопасный класс, с доступом через критическую секцию, что в нем плохого? А плохо то, что у нас доступны методы LockList и UnlockList. Если между парой вызовов LockList и UnlockList у нас будет синхронизация - то мы нарушаем вышеописанное правило. Поэтому выносить пару функций Lock/Unlock в паблик - не есть хорошо, и такие функции нужно использовать крайне осторожно. К слову, различные API от Microsoft часто возвращают Enum интерфейсы, вот например. Зачем они это делают? Ведь гораздо удобнее получить количество скажем через функцию Count, а потом в цикле через функцию GetItem по индексу получать элемент. Но в этом случае им бы пришлось вынести еще пару функций Lock/Unlock, чтобы никто не мог изменить список, пока вы в цикле работаете. Кроме того, если вы между Lock/Unlock вдруг вызовете такую API функцию, которая выполняет внутреннюю синхронизацию - вы запросто можете получить дедлок. Поэтому все и сделано через Enum интерфейсы. При получении такого интерфейса формируется список объектов, и счетчик ссылок их увеличивается. Это значит что ни один объект в Enum интерфейсе не будет уничтожен пока как минимум энум интерфейс существует, и пока вы работаете с Enum - ко внутреннему списку все имеют доступ, и этот список может даже изменяться.
Наверное хватитНажал я кнопку предпросмотр, увидел получившийся объем, и понял что пока хватит. В следующей статье я хотел бы рассказать про класс делфи TThread, и показать правила, которым я следую при создании и работе с потоками. Ссылки по теме |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
О нас |
Интернет-магазин ITShop.ru предлагает широкий спектр услуг информационных технологий и ПО.
На протяжении многих лет интернет-магазин предлагает товары и услуги, ориентированные на бизнес-пользователей и специалистов по информационным технологиям. Хорошие отзывы постоянных клиентов и высокий уровень специалистов позволяет получить наивысший результат при совместной работе. В нашем магазине вы можете приобрести лицензионное ПО выбрав необходимое из широкого спектра и ассортимента по самым доступным ценам. Наши менеджеры любезно помогут определиться с выбором ПО, которое необходимо именно вам. Также мы проводим учебные курсы. Мы приглашаем к сотрудничеству учебные центры, организаторов семинаров и бизнес-тренингов, преподавателей. Сфера сотрудничества - продвижение бизнес-тренингов и курсов обучения по информационным технологиям.
|
119334, г. Москва, ул. Бардина, д. 4, корп. 3 +7 (495) 229-0436 shopadmin@itshop.ru |
|
© ООО "Interface Ltd." Продаем программное обеспечение с 1990 года |