Достижение безопасности нитей - PullRequest
19 голосов
/ 19 февраля 2009

Вопрос Как я могу убедиться, что мое приложение поддерживает потоки? Есть ли у них какие-либо общие практики, методы тестирования, что следует избегать, что нужно искать?

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

mghie: Спасибо за ответ! Возможно, мне следует быть немного более точным. Просто чтобы прояснить, я знаю о принципах многопоточности, я использую синхронизацию (мониторы) во всей моей программе и знаю, как отличить проблемы потоков от других проблем реализации. Но, тем не менее, я постоянно забываю время от времени добавлять правильную синхронизацию. Просто для примера, я использовал функцию сортировки RTL в своем коде. Выглядело что-то вроде

FKeyList.Sort (CompareKeysFunc);

Оказывается, мне пришлось синхронизировать FKeyList при сортировке. Это просто не пришло мне в голову при первоначальной написании этой простой строки кода. Это то, о чем я хочу поговорить. В каких местах легко забыть добавить код синхронизации? Как ВЫ убедитесь, что вы добавили код синхронизации во все важные места?

Ответы [ 7 ]

16 голосов
/ 19 февраля 2009

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

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

Я бы сказал, начните со статьи Википедии о потоках и пролистайте связанные статьи. Я начал с книги "Многопоточное программирование Win32" Аарона Коэна и Майка Вудринга - она ​​вышла из печати, но, возможно, вы найдете что-то подобное.

Редактировать: Позвольте мне вкратце ответить на ваш отредактированный вопрос. Весь доступ к данным, которые не только для чтения, должны быть должным образом синхронизированы, чтобы быть потокобезопасным, и сортировка списка не является операцией только для чтения. Очевидно, что необходимо добавить синхронизацию для всех обращений к списку.

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

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

Что вы делаете, это создаете объекты, которые происходят от TInterfacedObject. Они реализуют один или несколько интерфейсов, которые обеспечивают только доступ только для чтения к внутренним объектам объекта, но они также могут предоставлять открытые методы, которые изменяют состояние объекта. При создании объекта вы сохраняете переменную типа объекта и переменную указателя интерфейса. Таким образом, управление временем жизни легко, поскольку объект будет автоматически удален при возникновении исключения. Переменная, указывающая на объект, используется для вызова всех методов, необходимых для правильной настройки объекта. Это изменяет внутреннее состояние, но так как это происходит только в активном потоке, нет никакой возможности для конфликта. Как только объект правильно настроен, вы возвращаете указатель интерфейса на вызывающий код, и поскольку впоследствии нет доступа к объекту, кроме как через указатель на интерфейс, вы можете быть уверены, что может быть выполнен только доступ только для чтения. Используя эту технику, вы можете полностью снять блокировку внутри объекта.

Что если вам нужно изменить состояние объекта? Вы не делаете, вы создаете новый, копируя данные из интерфейса, и затем изменяете внутреннее состояние новых объектов. Наконец вы возвращаете ссылочный указатель на новый объект.

При использовании этого вам потребуется блокировка только там, где вы получаете или устанавливаете такие интерфейсы. Это можно сделать даже без блокировки, используя функции атомного обмена. См. это сообщение в блоге от Primoz Gabrijelcic для аналогичного случая использования, когда установлен указатель интерфейса.

6 голосов
/ 19 февраля 2009

Просто: не используйте общие данные. Каждый раз, когда вы получаете доступ к общим данным, вы рискуете столкнуться с проблемой (если вы забудете синхронизировать доступ). Хуже того, каждый раз, когда вы получаете доступ к общим данным, вы рискуете заблокировать другие потоки, которые повредят вашу паралелизацию.

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

EDIT: более длинный ответ на комментарий Smasher. Не подходит в комментарии: (

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

2 голосов
/ 23 февраля 2009

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

Краткое руководство по овладению безопасностью резьбы

Конструкция для обеспечения безопасности резьбы

2 голосов
/ 19 февраля 2009

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

Я пытаюсь сделать следующее:

  • Используйте известный шаблон многопоточного проектирования : пул потоков , парадигма модели актера , шаблон команды или некоторые подобные подход. Таким образом, процесс синхронизации происходит одинаково, , единообразно , во всем приложении.
  • Ограничить и сконцентрировать точки синхронизации . Напишите свой код, чтобы вам была нужна синхронизация в как можно меньшем количестве мест, и сохраняйте код синхронизации в одном или нескольких местах кода.
  • Запишите код синхронизации так, чтобы логическое соотношение между значениями было ясным как при входе, так и при выходе из защиты. Я использую множество утверждений для этого (ваша среда может ограничить это).
  • Никогда не обращайтесь к общим переменным без защиты / синхронизации . Будьте предельно ясны, какие у вас общие данные. (я слышал, что существуют парадигмы для многопоточного программирования без защиты, но это потребует еще большего исследования).
  • Пишите ваш код максимально чисто, четко и сухо.
2 голосов
/ 19 февраля 2009

Я прислушаюсь ко второму совету Мги: безопасность нитей разработана. Читайте об этом где угодно.

Чтобы действительно низко понять, как это реализовано, поищите книгу о внутренностях ядра операционной системы реального времени. Хорошим примером является MicroC / OS-II: Ядро реального времени от Jean J. Labrosse, которое содержит полный аннотированный исходный код для рабочего ядра вместе с обсуждением причин сделали, как они есть.

Редактировать : В свете улучшенного вопроса об использовании функции RTL ...

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

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

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

1 голос
/ 19 февраля 2009

Мой простой ответ в сочетании с этими ответами:

  • Создайте приложение / программу, используя безопасная нить
  • Избегайте использования публичной статической переменной в все места

Поэтому он обычно легко попадает в эту привычку / практику, но ему нужно некоторое время, чтобы привыкнуть:

программируйте свою логику (не пользовательский интерфейс) на функциональном языке программирования, таком как F # или даже используя Scheme или Haskell. Кроме того, функциональное программирование поощряет практику обеспечения безопасности потоков, а также предупреждает нас о необходимости всегда соблюдать чистоту в функциональном программировании. Если вы используете F #, есть также четкое различие в использовании изменяемых или неизменяемых объектов, таких как переменные.


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

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

0 голосов
/ 07 июня 2010

M2C - Java-параллелизм на практике действительно хорош.

...