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

36

В первые дни FORTRAN и BASIC, по существу, все программы были написаны с заявлениями GOTO. Результатом стал код спагетти, а решение - структурированное программирование.

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

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

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

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

th_id = omp_get_thread_num();
#pragma omp critical
{
  cout << "Hello World from thread " << th_id << '\n';
}

Этот пример является выдержкой из: http://en.wikipedia.org/wiki/OpenMP

В качестве альтернативы аналогичная защита потоков друг от друга с использованием семафоров с функциями wait () и signal () может выглядеть следующим образом:

wait(sem);
th_id = get_thread_num();
cout << "Hello World from thread " << th_id << '\n';
signal(sem);

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

Устраняют ли такие системы, как OpenMP, проблемы с семафорами?
Перемещают ли они проблему куда-нибудь еще?
Как мне преобразовать мой любимый семафор, используя алгоритм, чтобы больше не использовать семафоры?

DeveloperDon
источник
О чем конкретно вы говорите? Что ты видел?
svick
4
Не хочу быть грубым, но вы могли бы исключить первые три абзаца. Они на самом деле не имеют отношения к вашему вопросу, и они переоценивают свои выводы и просто приведут много аргументов.
dbracey
1
Вау, большой редактировать. Я сделал удар в ответ. Этот вопрос все еще блуждает по GOTO, указателям, наследованию и полиморфизму, но в своем ответе я отложил эти проблемы в сторону и сосредоточился на вопросе «устаревшие практики».
Стюарт Маркс

Ответы:

15

Существуют ли параллельные методы и практики программирования, которые больше не следует использовать? Я бы сказал, да .

Одним из ранних методов параллельного программирования, который сегодня кажется редким, является программирование на основе прерываний . Вот как UNIX работал в 1970-х годах. См. Комментарий Льва о UNIX или о дизайне Баха операционной системы UNIX . Вкратце, техника заключается в том, чтобы временно приостанавливать прерывания при манипулировании структурой данных, а затем восстанавливать прерывания после этого. Страница справочника BSD spl (9)есть пример этого стиля кодирования. Обратите внимание, что прерывания ориентированы на аппаратное обеспечение, а код воплощает неявную связь между типом аппаратного прерывания и структурами данных, связанными с этим оборудованием. Например, код, который управляет буферами дискового ввода-вывода, должен приостанавливать прерывания от оборудования контроллера диска при работе с этими буферами.

Этот стиль программирования использовался операционными системами на однопроцессорном оборудовании. Для приложений было гораздо реже иметь дело с прерываниями. В некоторых ОС были программные прерывания, и я думаю, что люди пытались построить поверх них системы потоков или сопрограмм, но это было не очень широко распространено. (Конечно, не в мире UNIX.) Я подозреваю, что программирование в стиле прерываний сегодня ограничивается небольшими встроенными системами или системами реального времени.

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

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

Существуют дополнительные библиотечные конструкции, такие как в java.util.concurrentпакете Java , которые включают в себя множество высококонкурентных структур данных и конструкций пула потоков. Они могут быть объединены с дополнительными методами, такими как удержание нити и эффективная неизменность. См. Java Concurrency In Practice by Goetz et. и др. для дальнейшего обсуждения. К сожалению, многие программисты все еще используют свои собственные структуры данных с блокировками и условиями, когда им действительно нужно просто использовать что-то вроде ConcurrentHashMap, где тяжелая работа уже была проделана авторами библиотеки.

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

К сожалению, не совсем ясно, можно ли этого избежать во всех случаях. Много программирования все еще делается таким образом. Было бы неплохо увидеть это вытесненным чем-то другим. Ответы Джаррода Роберсона и davidk01 указывают на такие методы, как неизменяемые данные, функциональное программирование, STM и передача сообщений. Их можно много рекомендовать, и все они активно развиваются. Но я не думаю, что они полностью заменили старое доброе старое изменяемое состояние.

РЕДАКТИРОВАТЬ: вот мой ответ на конкретные вопросы в конце.

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

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

Стюарт Маркс
источник
Спасибо, это отличная информация. Я рассмотрю ссылки и углублюсь в концепции, которые вы упоминаете, которые являются новыми для меня.
DeveloperDon
+1 за java.util.concurrent и согласился с комментарием - он был в JDK с 1.5, и я редко, если когда-либо видел, чтобы он использовался.
MebAlone
1
Я хотел бы, чтобы вы подчеркнули, как важно не создавать свои собственные структуры, когда они уже существуют. Так много, так много ошибок ...
CorsiKa
Я не думаю, что было бы правильно сказать: «Семафоры опережают прерывания, потому что они являются программными конструкциями (не связанными с аппаратным обеспечением) ». Семафоры зависят от процессора для реализации инструкции Compare-and-Swap или ее многоядерных вариантов .
Джош Пирс,
@JoshPearce Конечно, семафоры реализованы с использованием аппаратных конструкций, но они являются абстракцией, которая не зависит от какой-либо конкретной аппаратной конструкции, такой как CAS, test-and-set, cmpxchng и т. Д.
Stuart Marks
28

Ответ на вопрос

Общим согласием является то, что общее изменяемое состояние - это Bad ™, а неизменное состояние - это Good ™, что снова и снова подтверждается и подтверждается функциональными и императивными языками.

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

Ущербное помещение

Этот вопрос основан на сравнении с ошибочной предпосылкой; это GOTOбыла актуальная проблема, которая была признана Всемирным советом дизайнеров языков и союзов разработчиков программного обеспечения универсально ©! Без GOTOмеханизма ASM не будет работать вообще. То же самое можно сказать и о том, что необработанные указатели являются проблемой в C или C ++, а некоторые считают, что умные указатели являются панацеей, но это не так.

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

Образование - это панацея

Идиотские программисты - это то deprecated, что было , у каждого популярного языка все еще есть GOTOконструкция, прямо или косвенно, и это best practiceкогда правильно используется в каждом языке, который имеет этот тип конструкций.

ПРИМЕР: в Java есть метки, и try/catch/finallyобе они напрямую работают как GOTOоператоры.

Большинство Java-программистов, с которыми я общаюсь, даже не знают, что на immutableсамом деле означает вне их, повторяя the String class is immutableс зомби-взглядом в их глазах. Они определенно не знают, как правильно использовать finalключевое слово для создания immutableкласса. Поэтому я почти уверен, что они понятия не имеют, почему передача сообщений с использованием неизменяемых сообщений так хороша, и почему общее изменяемое состояние не так велико.

Сообщество
источник
3
+1 Отличный ответ, четко написанный и точно определяющий основную картину изменчивого состояния. IUBLDSEU должен стать мемом :)
Диббеке
2
GOTO - это кодовое слово для «пожалуйста, нет, пожалуйста, не начинайте пламенную войну здесь, я вас догоняю». Этот вопрос разжигает огонь, но на самом деле не дает хорошего ответа. Похвальные упоминания о функциональном программировании и неизменности замечательны, но эти заявления не имеют смысла.
Эван Плейс
1
Это кажется противоречивым ответом. Сначала вы говорите: «А это плохо, В - это хорошо», затем вы говорите: «Идиоты устарели». Разве это не относится к первому абзацу? Разве я не могу просто взять последнюю часть вашего ответа и сказать: «Общее изменяемое состояние - это лучшая практика при правильном использовании на каждом языке». Также «доказательство» - очень сильное слово. Вы не должны использовать его, если у вас нет действительно веских доказательств.
luiscubal
2
Я не собирался начинать пламенную войну. Пока Джаррод не отреагировал на мой комментарий, он думал, что GOTO не вызывает сомнений и будет хорошо работать по аналогии. Когда я писал вопрос, он не приходил мне в голову, но Дейкстра находился в эпицентре как GOTO, так и семафоров. Эдсгер Дейкстра кажется мне гигантом, и ему приписывают изобретение семафоров (1965) и раннюю (1968) научную работу о GOTO. Метод адвокатуры Дейкстры часто был грубым и конфронтационным. Противоречие / конфронтация работали на него, но я просто хочу идеи о возможных альтернативах семафорам.
DeveloperDon
1
Многие программы должны моделировать вещи, которые в реальном мире изменчивы. Если в 5:37 утра объект № 451 удерживает состояние чего-либо в реальном мире в этот момент (5:37 утра), а затем состояние объекта реального мира изменяется, это возможно для идентичности объекта, представляющего состояние реальной вещи, которая должна быть неизменной (т. е. вещь всегда будет представлена ​​объектом # 451), или объект # 451 должен быть неизменным, но не оба. Во многих случаях наличие неизменяемой идентичности будет более полезным, чем наличие неизменного объекта # 451.
Суперкат
27

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

Эрланг использует передачу сообщений и агентов для параллелизма, и это более простая модель для работы, чем STM. С передачей сообщений вам абсолютно не нужно беспокоиться о блокировках и семафорах, потому что каждый агент работает в своей собственной мини-вселенной, поэтому нет связанных с данными условий гонки. У вас все еще есть некоторые странные крайние случаи, но они далеко не такие сложные, как живые и тупиковые блокировки. Языки JVM могут использовать Akka и получать все преимущества передачи сообщений и актеров, но в отличие от Erlang JVM не имеет встроенной поддержки акторов, поэтому в конце дня Akka по-прежнему использует потоки и блокировки, но вы как программисту не нужно беспокоиться об этом.

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

Я не уверен, насколько эта технология доступна в C ++, но есть вероятность, что если вы видите что-то, что не использует явно потоки и блокировки, то это будет один из вышеперечисленных методов управления параллелизмом.

davidk01
источник
+1 за новый термин "волосатые детали". LOL человек. Я просто не могу перестать смеяться над этим новым термином. Я думаю, что теперь я буду использовать «волосатый код».
Саид Нимати
1
@Seed: я слышал это выражение раньше, это не так уж редко. Я согласен, что это смешно :-)
Кэмерон
1
Хороший ответ. .NET CLI предположительно также имеет поддержку для сигнализации (в отличие от блокировки), но мне еще не приходилось сталкиваться с примером, где он полностью заменил блокировку. Я не уверен, считается ли асинхронный. Если вы говорите о таких платформах, как Javascript / NodeJ, то они на самом деле однопоточные и лучше работают только при высоких нагрузках ввода-вывода, потому что они гораздо менее подвержены максимальному ограничению ресурсов (т. Е. В тонне одноразовых контекстов). При интенсивной загрузке ЦП использование асинхронного программирования не дает никаких преимуществ.
Эван Плейс
1
Интересный ответ, я не сталкивался с фьючерсами раньше. Также обратите внимание, что вы все еще можете иметь взаимоблокировку и живую блокировку в системах передачи сообщений, таких как Erlang . CSP позволяет формально рассуждать о взаимоблокировках и живых блокировках, но сам по себе не предотвращает их.
Марк Бут
1
Я бы добавил Lock free и дождался свободных структур данных в этом списке.
каменный металл
3

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

Это относится и к структурам управления: ifs, forS и даже try- catchблоки просто абстракции над gotoс. Эти абстракции почти всегда полезны, потому что они делают ваш код более читабельным. Но есть случаи, когда вам все равно придется использовать goto(например, если вы пишете сборку вручную).

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

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

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

svick
источник
Спасибо, вы уловили аналогию, и у меня нет предвзятого представления или даже топора о том, что ответ семафоров WRT заключается в том, являются ли они устаревшими или нет. Больше вопросов для меня: есть ли лучшие способы и в системах, в которых семафорам, по-видимому, не хватает чего-то важного, и они были бы не в состоянии выполнить весь спектр многопоточных алгоритмов.
DeveloperDon
2

Да, но вы вряд ли столкнетесь с некоторыми из них.

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

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

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

Алекс Фейнман
источник
Спасибо. Кажется, что есть большой потенциал для использования параллельного программирования, но это может быть ящик Пандоры, если не использовать дисциплинированным образом.
DeveloperDon
2

Grand Central Dispatch от Apple - это элегантная абстракция, которая изменила мои представления о параллелизме. Сосредоточение на очередях делает реализацию асинхронной логики на порядок проще, по моему скромному опыту.

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

orip
источник
1

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

Это одна из причин, почему асинхронные или основанные на задачах (например, Grand Central Dispatch или Intel TBB) платформы более популярны, они выполняют задачу кода 1 за один раз, выполняя ее до перехода к следующей - однако вы должны кодировать каждую каждая задача занимает немного времени, если вы не хотите испортить дизайн (т.е. ваши параллельные задачи действительно поставлены в очередь). Задачи с интенсивным использованием ЦП передаются на альтернативное ядро ​​ЦП, а не обрабатываются в одном потоке, обрабатывающем все задачи. Его также легче управлять, если не происходит действительно многопоточная обработка.

gbjbaanb
источник
Круто, спасибо за ссылки на технологии Apple и Intel. Ваш ответ указывает на проблемы управления потоком к основной близости? Некоторые проблемы с производительностью кэша уменьшены, потому что многоядерные процессоры могут повторять кэши L1 на ядро? Например: software.intel.com/en-us/articles/…. Высокоскоростной кэш для четырех ядер с большим количеством обращений к кэшу может быть более чем в 4 раза быстрее, чем одно ядро ​​с большим количеством пропусков кеша для одних и тех же данных. Матрица умножения может. Случайное планирование 32 потоков на 4 ядра не может. Давайте использовать сходство и получить 32 ядра.
DeveloperDon
хотя на самом деле это не та же проблема - сродство ядра относится только к проблеме, когда задача переходит от ядра к ядру. Это та же проблема, если задача прерывается, заменяется новой, тогда исходная задача продолжается в том же ядре. Intel говорит там: попадание в кэш = быстро, пропуск в кэш = медленно, независимо от количества ядер. Я думаю, что они пытаются убедить вас покупать их чипы, а не AMD :)
gbjbaanb