Как объяснить, почему многопоточность сложна

84

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

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

Итак, как я могу представить его или объяснить, почему он может недооценивать сложности параллелизма, параллелизма и многопоточности? Или может я не прав?

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

Мистер Шоубс
источник
17
Многопоточность проста. Правильная синхронизация трудна.
Вине Рейнольдс
33
Введите трех человек в комнату, желательно с разными акцентами, и попросите их объяснить разные, частично совпадающие части проблемы параллелизма ... одновременно.
Greyfade
Многопоточность может быть очень сложной или очень простой, в зависимости от имеющейся проблемы и языковой поддержки. Clojure облегчает работу clojure.org/concurrent_programming
работа
4
@Job Параллельное программирование всегда сложно (в реальных проектах), независимо от того, какой язык вы используете. Scala, Clojure или Erlang делают его немного вменяемым, когда вы хотите сравнить его с языками, которые используют и поддерживают изменяемые состояния.
Хирон
4
Моя любимая метафора для этого: «Примешь ли ты снотворное и слабительное одновременно?» Даже при использовании сложных очередей сообщений порядок - результат параллелизма, выполненного правильно . Это, если у вас нет большого опыта с этим, трудно для многих людей.
Тим Пост

Ответы:

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

  2. «Забей на него» - тривиальное решение ... оно решает все твои проблемы, если ты не заботишься о производительности. Попытайтесь проиллюстрировать, какой удар по производительности был бы, если бы, например, Amazon пришлось блокировать все восточное побережье, когда кто-то в Атланте заказывает одну книгу!

Килиан Фот
источник
1
+1 за обсуждение математической сложности - вот как я понял сложность параллелизма с общим состоянием, и я обычно привожу аргумент в защиту архитектуры передачи сообщений. -1 для «ударить по нему блокировку» ... Фраза означает бездумный подход к использованию блокировок, что очень вероятно приведет к тупику или противоречивому поведению (поскольку клиенты вашего кода, которые живут в разных потоках, конфликтуют запросы, но не синхронизируются между собой, клиенты будут иметь несовместимые модели состояния вашей библиотеки).
Эйдан Калли
2
Amazon делает должна зафиксировать инвентаризацию отдельного элемента на одном складе кратко при обработке заказа. Если на одном конкретном предмете произойдет внезапный огромный скачок, эффективность заказа для этого предмета будет снижаться до тех пор, пока запас не будет исчерпан и доступ к инвентарю не станет доступным только для чтения (и, следовательно, на 100% совместное использование). У Amazon есть одна вещь, которую другие программы не делают - это возможность ставить заказы в очередь до тех пор, пока не произойдет пополнение запасов, и возможность обслуживания очередей до того, как пополнение будет доступно для новых заказов.
Blrfl
@Blrfl: Программы могут сделать это, если они написаны для использования передачи сообщений через очереди. Нет необходимости, чтобы все сообщения для определенной темы проходили через одну очередь ...
Donal Fellows
4
@Donal Fellows: если на складе есть 1M виджетов и 1M заказов поступают в одно и то же время, все эти запросы сериализуются на некотором уровне при сопоставлении элементов с заказами независимо от того, как они обрабатываются. Практическая реальность такова, что у Amazon, вероятно, никогда не бывает так много виджетов на складе, что задержка в обработке большого количества заказов становится неприемлемо высокой, пока не закончится инвентаризация, и всем остальным в очереди можно будет сказать (параллельно): «Мы ушли. " Очереди сообщений - отличный способ предотвратить взаимные блокировки, но они не решают проблему высокой конкуренции за ограниченный ресурс.
Blrfl
79

Многопоточность является простой. Кодирование приложения для многопоточности очень и очень просто.

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

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

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

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

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

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

Вот игра. Каждый из вас читает формулу, пишет ответ и ставит карточку с ответом.

Каждый из вас сделает половину работы, верно? Вы сделали в половине случаев, верно?

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

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

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

С. Лотт
источник
Даже разрезание бумаги в стопку не может полностью исправить ситуацию - у вас все еще есть ситуация, когда вы и ваш босс одновременно добиваетесь новой формулы, и вы вбиваете кулаки в его. На самом деле, я бы сказал, что это типичная проблема с многопоточностью. Действительно грубые ошибки обнаруживаются рано. Действительно необычные ошибки остаются навсегда, потому что никто не может их воспроизвести. Правдоподобные условия гонки - подобные этой - продолжают появляться в тестировании, и в конечном итоге все (или, скорее всего, большинство) из них устраняются.
Airsource Ltd 14.12.15
@AirsourceLtd Что именно вы говорите, "ударить кулаками в его"? Пока у вас есть очередь сообщений, которая не позволяет двум разным потокам принимать одно и то же сообщение, это не будет проблемой. Если я не понимаю, что вы имели в виду.
Зак
25

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

Существует ряд подходов, таких как модель актора или (программная) транзакционная память , которые намного проще. Или работа с неизменяемыми структурами данных (такими как списки и деревья).

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

Так что да, технически многопоточность сложна.
«Slap a lock on» - это в значительной степени наименее масштабируемое решение проблем параллелизма, которое фактически сводит на нет всю цель многопоточности. Что он делает, так это возвращает проблему обратно к неконкурентной модели выполнения. Чем больше вы это делаете, тем больше вероятность того, что у вас работает только один поток (или 0 в тупике). Это побеждает всю цель.
Это все равно, что сказать: «Решение проблем третьего мира легко. Просто бросьте в него бомбу». То, что существует тривиальное решение, не делает проблему тривиальной, поскольку вы заботитесь о качестве результата.

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

back2dos
источник
14

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

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

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

sunwukung
источник
1
Интересный ответ, хотя это технический вопрос. Однако я согласен с тем, что вы говорите ... в этом случае мой менеджер довольно хороший программист, однако я просто думаю, что он не сталкивался со сложностями многопоточных приложений, он их недооценивает.
Мистер Шоубс,
6

Один простой мысленный эксперимент для понимания тупиков - это проблема « столового философа ». Один из примеров, которые я склонен использовать для описания того, насколько плохими могут быть условия гонки, - это ситуация с Therac 25 .

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

Tangurena
источник
1
т.е. проблема с сэндвичем: вы делаете кучу бутербродов, но есть только 1 блюдо с маслом и 1 нож. Обычно все в порядке, но в конце концов кто-то схватит масло, а кто-то схватит нож ... и тогда они оба будут стоять там, ожидая, пока другой отпустит свой ресурс.
gbjbaanb
Могут ли такие тупиковые проблемы быть решены путем постоянного получения ресурсов в установленном порядке?
комп
@ compman, нет. Потому что два потока могут пытаться получить доступ к одному и тому же ресурсу в одно и то же время, и эти потоки не обязательно нуждаются в одном и том же наборе ресурсов - достаточно перекрытия, чтобы вызвать проблемы. Одна из схем состоит в том, чтобы вернуть ресурс назад, а затем подождать случайный период, прежде чем снова его захватить. Этот период отката происходит в ряде протоколов, самым ранним из которых был Aloha. en.wikipedia.org/wiki/ALOHAnet
Tangurena
1
Что если каждый ресурс в программе имеет номер, а когда потоку / процессу требуется набор ресурсов, он всегда блокирует ресурсы в возрастающем числовом порядке? Я не думаю, что этот тупик может случиться.
комп
1
@compman: Это действительно способ избежать тупика. Можно разработать инструменты, которые позволят вам автоматически проверить это; так что если ваше приложение никогда не обнаруживает блокировку ресурсов, кроме как в порядке возрастания номеров, то у вас никогда не было потенциальной тупиковой ситуации. (Обратите внимание, что потенциальные взаимоблокировки превращаются в реальные взаимоблокировки только тогда, когда ваш код выполняется на компьютере клиента).
gnasher729
3

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

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

mattnz
источник
3

Краткий ответ в двух словах: НАБЛЮДАЕМЫЙ НОНДЕТЕРМИНИЗМ

Длинный ответ: это зависит от того, какой подход к параллельному программированию вы используете, учитывая вашу проблему. В книге « Концепции, методы и модели компьютерного программирования» авторы четко объясняют четыре основных практических подхода к написанию параллельных программ:

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

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

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

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

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

Jernej Jerin
источник
0

Сначала меня учили, что это может вызвать проблемы, увидев простую программу, которая запускает 2 потока и одновременно выводит их на консоль с 1 по 100. Вместо:

1
1
2
2
3
3
...

Вы получите что-то более похожее на это:

1
2
1
3
2
3
...

Запустите его снова, и вы можете получить совершенно другие результаты.

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

Морган Херлокер
источник
-3

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

Эскалируйте это к строительству дома.

Не спите по ночам, воображая, что вы архитектор. :)

Маке
источник
-3

Простая часть: использование многопоточности с современными функциями сред, операционных систем и аппаратного обеспечения, таких как семафоры, очереди, блокированные счетчики, атомарные типы в штучной упаковке и т. Д.

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


источник
3
Тяжелая часть действительно сложнее, но даже эта легкая часть не так легка.
PeterAllenWebb