Потоки Best Practices - PullRequest
       44

Потоки Best Practices

42 голосов
/ 19 марта 2009

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

Я вроде как шаблон дизайна или что-то в этом роде.

Ответы [ 11 ]

66 голосов
/ 19 марта 2009

(Предполагается, что .NET; аналогичные вещи применимы и для других платформ.)

Ну, есть много вещей, которые нужно учитывать. Я бы посоветовал:

  • Неизменность отлично подходит для многопоточности. Функциональное программирование работает хорошо одновременно отчасти из-за акцента на неизменности.
  • Используйте блокировки при доступе к изменяемым общим данным как для чтения, так и для записи.
  • Не пытайтесь освободиться от блокировки, если вам действительно не нужно. Замки стоят дорого, но редко являются узким местом.
  • Monitor.Wait должен почти всегда быть частью цикла условия, ожидая, пока условие станет истинным, и снова ожидая, если оно не будет.
  • Старайтесь избегать удерживать замки дольше, чем нужно.
  • Если вам когда-нибудь понадобится приобрести два замка одновременно, тщательно документируйте заказ и убедитесь, что вы всегда используете один и тот же заказ.
  • Документирование безопасности потоков ваших типов. Большинство типов не должны быть поточно-ориентированными, они просто не должны быть враждебными по отношению к потокам (т. Е. «Вы можете использовать их из нескольких потоков, но это ваша ответственность за устранение» блокирует, если вы хотите поделиться ими)
  • Не обращайтесь к пользовательскому интерфейсу (кроме документированных поточно-ориентированных способов) из не-пользовательского потока. В Windows Forms используйте Control.Invoke / BeginInvoke

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

33 голосов
/ 19 марта 2009

Научиться правильно писать многопоточные программы чрезвычайно сложно и отнимает много времени.

Итак, первый шаг: замените реализацию той, которая вообще не использует несколько потоков.

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

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

Старайтесь не распылять блоки lock вокруг вашего кода в надежде, что он станет поточно-ориентированным. Не работает В конце концов, два пути к коду получат одинаковые блокировки в другом порядке, и все будет остановлено (раз в две недели на сервере клиента). Это особенно вероятно, если вы объединяете потоки с событиями запуска и удерживаете блокировку во время запуска события - обработчик может снять другую блокировку, и теперь у вас есть пара блокировок, удерживаемых в определенном порядке. Что если их вывезут в обратном порядке в другой ситуации?

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

Вместо этого купите эту книгу .

Вот очень красиво сформулированное резюме с этого сайта :

Многопоточность также поставляется с недостатки. Самое большое, что это может привести к гораздо более сложному программы. Наличие нескольких потоков делает не само по себе создает сложность; его взаимодействие между потоками это создает сложность. Это относится является ли взаимодействие намеренный, и может привести к долгому циклы развития, а также постоянная подверженность перемежающемуся и невоспроизводимые ошибки. За это причина, это стоит держать такой взаимодействие в многопоточной конструкции просто - или не использовать многопоточность в все - если у вас нет своеобразного склонность к переписыванию и отладке!

Идеальное резюме от Страуструпа :

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

14 голосов
/ 19 марта 2009

(Как и Джон Скит, большая часть этого предполагает .NET)

Имея риск показаться спорным, комментарии, подобные этим, просто беспокоят меня:

Учимся писать многопоточные программы правильно крайне сложно и отнимает много времени.

Темы следует избегать, когда возможно ...

Практически невозможно написать программное обеспечение, которое делает что-либо существенное, не используя потоки в некотором объеме. Если вы работаете в Windows, откройте диспетчер задач, включите столбец «Количество потоков», и вы, вероятно, сможете посчитать с одной стороны количество процессов, которые используют один поток. Да, не следует просто использовать потоки ради использования потоков и не следует делать это кавалерно, но, честно говоря, я считаю, что эти штампы используются слишком часто.

Если бы мне пришлось сводить многопоточное программирование к истинному новичку, я бы сказал так:

  • Прежде чем перейти к нему, сначала поймите, что граница класса не совпадает с границей потока. Например, если метод обратного вызова в вашем классе вызывается другим потоком (например, делегатом AsyncCallback методу TcpListener.BeginAcceptTcpClient ()), следует понимать, что обратный вызов выполняет в этого другого потока. Таким образом, даже если обратный вызов происходит для того же объекта, вам все равно придется синхронизировать доступ к членам объекта в методе обратного вызова. Потоки и классы ортогональны; важно понять этот момент.
  • Определите, какие данные должны быть разделены между потоками. После того, как вы определили общие данные, попробуйте объединить их в один класс, если это возможно.
  • Ограничьте места, где общие данные могут быть записаны и прочитаны. Если вы сможете свалить это в одно место для письма и в одно место для чтения, вы окажете себе огромную услугу. Это не всегда возможно, но это хорошая цель для стрельбы.
  • Очевидно, что вы синхронизируете доступ к общим данным, используя класс Monitor или ключевое слово lock.
  • Если возможно, используйте один объект для синхронизации ваших общих данных независимо от того, сколько существует различных общих полей. Это упростит вещи. Однако это также может чрезмерно ограничивать вещи, и в этом случае вам может понадобиться объект синхронизации для каждого общего поля. И в этот момент использование неизменяемых классов становится очень удобным.
  • Если у вас есть один поток, который должен сигнализировать о другом потоке, я настоятельно рекомендую использовать класс ManualResetEvent, чтобы сделать это вместо использования событий / делегатов.

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

EDIT: В ThreadPool.QueueUserWorkItem (), асинхронных делегатах, различных парах методов BeginXXX / EndXXX и т. Д. В C # нет ничего «чрезвычайно сложного». Во всяком случае, эти методы делают намного проще для выполнения различных задач в многопоточном режиме. Если у вас есть приложение с графическим интерфейсом, которое выполняет какие-либо тяжелые взаимодействия с базой данных, сокетами или вводом / выводом, практически невозможно заставить внешний интерфейс реагировать на пользователя без использования потоков за кулисами. Методы, которые я упомянул выше, делают это возможным и легким в использовании. Важно понимать подводные камни, чтобы быть уверенным. Я просто считаю, что мы делаем программистов, особенно младших, плохой услугой, когда говорим о том, насколько «чрезвычайно сложным» является многопоточное программирование или как «избегать потоков». Подобные комментарии упрощают проблему и преувеличивают миф, когда правда заключается в том, что потоки никогда не были проще. Есть законные причины для использования тем, и подобные клише мне кажутся контрпродуктивными.

6 голосов
/ 19 марта 2009

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

Еще одна полезная практика, которую проще адаптировать к существующему коду, - назначать приоритет или уровень каждой блокировке в вашей системе и обеспечивать последовательное соблюдение следующих правил:

  • Удерживая замок на уровне N, вы могут приобретать только новые замки нижних уровней
  • Несколько замков на одном уровне должны приобретаться одновременно, как одиночная операция, которая всегда пытается приобрести все запрошенные блокировки в тот же глобальный порядок (обратите внимание, что любой последовательный порядок подойдет, но любой нить, которая пытается получить один или больше замков на уровне N, должны сделать приобретать их в том же порядке, что и любой другой поток будет делать в другом месте в коде.)

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

4 голосов
/ 19 марта 2009

БОЛЬШОЙ акцент на первом пункте, который отправил Джон. Чем более неизменным является ваше состояние (т. Е. Глобальные переменные и т. Д.), Тем легче будет ваша жизнь (т. Е. Чем меньше блокировок вам придется иметь дело, тем меньше у вас аргументов) делать с чередованием порядка и т.д ...)

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

2 голосов
/ 19 марта 2009

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

Я бы, наверное, согласился со всеми опубликованными мнениями. Кроме того, я бы порекомендовал использовать некоторые существующие более грубые структуры, обеспечивающие строительные блоки, а не простые средства, такие как блокировки или операции ожидания / уведомления. Для Java это будет просто встроенный пакет java.util.concurrent, который дает вам готовые к использованию классы, которые вы можете легко комбинировать для создания многопоточного приложения. Большим преимуществом этого является то, что вы избегаете написания низкоуровневых операций, что приводит к сложному для чтения и подверженному ошибкам коду, в пользу гораздо более четкого решения.

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

1 голос
/ 20 марта 2009

Это изменчивое состояние, глупо

Это прямая цитата из Java-параллелизма на практике Брайана Гетца. Несмотря на то, что книга ориентирована на Java, «Краткое содержание части I» дает некоторые другие полезные советы, которые будут применяться во многих контекстах многопоточного программирования. Вот еще несколько из этого же резюме:

  • Неизменяемые объекты автоматически поточно-ориентированы.
  • Защита каждой изменяемой переменной с помощью блокировки.
  • Программа, которая обращается к изменяемой переменной из нескольких потоков без синхронизация не работает.

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

alt text
(источник: umd.edu )

1 голос
/ 19 марта 2009

Ну, все до сих пор были ориентированы на Windows / .NET, так что я включу немного Linux / C.

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

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

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

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

1 голос
/ 19 марта 2009

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


Некоторые разработчики считают, что «почти достаточно» блокировки достаточно хорошо. По моему опыту, может быть и обратное - блокировка «почти достаточно» может быть хуже , чем достаточно блокировки.

Представьте себе поток Блокирующий ресурс R, использующий его, а затем разблокирующий его. А затем использует ресурс R 'без блокировки.

Тем временем поток B пытается получить доступ к R, пока A заблокировал его. Поток B блокируется до тех пор, пока поток A не разблокирует R. Затем контекст ЦП переключается на поток B, который обращается к R, а затем обновляет R 'в течение своего временного интервала . Это обновление делает R 'несовместимым с R, вызывая ошибку, когда A пытается получить к нему доступ.


Тестирование на максимально возможном количестве аппаратных и ОС архитектур. Различные типы процессоров, разное количество ядер и чипов, Windows / Linux / Unix и т. Д.


Первым разработчиком, который работал с многопоточными программами, был парень по имени Мерфи.

1 голос
/ 19 марта 2009

Я хотел бы проконсультироваться с советом Джона Скита еще парой советов:

  • Если вы пишете «сервер» и, вероятно, у вас большой параллелизм вставки, не используйте Microsoft Compact SQL. Менеджер блокировок тупой. Если вы используете SQL Compact, НЕ используйте сериализуемые транзакции (что по умолчанию используется для класса TransactionScope). Вещи развалится на вас быстро. SQL Compact не поддерживает временные таблицы, и когда вы пытаетесь смоделировать их внутри сериализованных транзакций, он делает глупые вещи, такие как взятие x-lock на страницах индекса таблицы _sysobjects. Кроме того, он действительно заинтересован в продвижении блокировки, даже если вы не используете временные таблицы. Если вам нужен последовательный доступ к нескольким таблицам, лучше всего использовать повторяющиеся транзакции чтения (чтобы обеспечить атомарность и целостность), а затем внедрить собственный менеджер иерархической блокировки на основе объектов домена (учетные записи, клиенты, транзакции и т. Д.), А не используя схему базы данных на основе таблицы строк.

    Однако, когда вы делаете это, вы должны быть осторожны (как сказал Джон Скит), чтобы создать четко определенную иерархию блокировок.

  • Если вы создаете свой собственный менеджер блокировок, используйте поля <ThreadStatic> для хранения информации о блокировках, которые вы принимаете, а затем добавляйте утверждения везде в диспетчере блокировок, которые применяют ваши правила иерархии блокировок. Это поможет искоренить потенциальные проблемы заранее.

  • В любом коде, который выполняется в потоке пользовательского интерфейса, добавьте утверждения в !InvokeRequired (для winforms) или Dispatcher.CheckAccess() (для WPF). Аналогичным образом следует добавить обратное утверждение в код, который выполняется в фоновых потоках Таким образом, люди, смотрящие на метод, узнают, просто взглянув на него, каковы его требования к потокам. Утверждения также помогут выявить ошибки.

  • Утверждай как сумасшедший, даже в розничных сборках. (это означает бросок, но вы можете сделать свои броски похожими на утверждения). Аварийный дамп с исключением, в котором говорится, что «вы нарушили правила многопоточности, выполняя это», наряду со следами стека, намного легче отладить, чем отчет клиента из другой части мира, который время от времени говорит «приложение». просто замерзает на мне, или выплевывает сожрал ".

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...