Подтвердить что ты не робот

Обеспечение безопасности потоков

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

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

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

FKeyList.Sort (CompareKeysFunc);

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

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

Изменить. В свете улучшенного вопроса, посвященного использованию функции RTL...

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

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

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

Ответ 4

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

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

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

Ответ 6

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

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

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

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


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

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