Рекомендации по переработке резьбы

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

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

Ответ 1

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

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

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

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

Ответ 2

Обучение написанию многопоточных программ выполняется очень сложно и требует много времени.

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

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

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

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

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

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

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

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

Отличное резюме от Stroustrup:

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

Ответ 3

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

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

Обучение написанию многопоточных программ правильно трудно и трудоемко.

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

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

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

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

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

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

Ответ 4

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

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

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

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

Ответ 5

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

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

Ответ 6

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

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

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

Ответ 7

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

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

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

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

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

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

Ответ 8

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


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

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

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


Протестируйте как можно больше различных аппаратных и OS-архитектур. Различные типы процессоров, различное количество ядер и чипов, Windows/Linux/Unix и т.д.


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

Ответ 9

Ну, все до сих пор были ориентированы на Windows/.NET, поэтому я буду звонить с Linux/C.

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

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

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

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

Ответ 10

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

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

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

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

alt text http://www.cs.umd.edu/class/fall2008/cmsc433/jcipMed.jpg

Ответ 11

Вместо блокировки на контейнерах вы должны использовать ReaderWriterLockSlim. Это дает вам базу данных, такую ​​как блокировка - бесконечное количество читателей, один писатель и возможность обновления.

Что касается шаблонов проектирования, pub/sub довольно хорошо зарекомендовал себя и очень легко писать в .NET(используя readerwriterlockslim). В нашем коде у нас есть объект MessageDispatcher, который получает каждый. Вы подписываетесь на него, или вы отправляете сообщение полностью асинхронным образом. Все, что вам нужно для блокировки, это зарегистрированные функции и любые ресурсы, над которыми они работают. Это упрощает многопоточность.