Почему многопоточность часто предпочтительнее для повышения производительности?

23

У меня вопрос, почему программисты любят параллельные и многопоточные программы вообще.

Я рассматриваю 2 основных подхода:

  • асинхронный подход, основанный в основном на сигналах, или просто асинхронный подход, как его называют многие документы и языки, например, новый C # 5.0, и «сопутствующий поток», который управляет политикой вашего конвейера.
  • параллельный подход или многопоточный подход

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

Я протестировал многопоточные программы и асинхронные программы на старой машине с четырехъядерным процессором Intel, который не имеет контроллера памяти внутри ЦП, память полностью управляется материнской платой, и в этом случае производительность ужасна. Многопоточное приложение, даже относительно небольшое количество потоков, таких как 3-4-5, может быть проблемой, приложение не отвечает и просто медленно и неприятно.

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

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

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

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

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

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

user1849534
источник
2
Одним из способов сравнения было бы посмотреть на мир JavaScript, где нет многопоточности и все агрессивно асинхронно с использованием обратных вызовов. Это работает, но у него есть свои проблемы.
Gort the Robot
2
@StevenBurnap Что вы называете веб-работниками?
user16764 15.12.12
2
«Даже относительно небольшое количество потоков, таких как 3-4-5, может быть проблемой, приложение не отвечает и просто медленно и неприятно». => Это может быть связано с плохим дизайном / неправильным использованием потоков. Обычно вы сталкиваетесь с такой ситуацией, когда ваши потоки продолжают обмениваться данными, и в этом случае многопоточность может быть неправильным ответом или вам может потребоваться переразметить данные.
assylias
1
@assylias Чтобы увидеть значительное замедление в потоке пользовательского интерфейса указывает на чрезмерную блокировку потоков. У вас либо плохая реализация, либо вы пытаетесь втиснуть квадратный колышек в круглое отверстие.
Эван Плейс
5
Вы говорите: «Программисты, кажется, любят параллелизм и многопоточные программы в целом», я сомневаюсь в этом. Я бы сказал «программисты ненавидят это» ... но часто это единственная полезная вещь, которую можно сделать ...
Йоханнес

Ответы:

34

У вас есть несколько ядер / процессоров, используйте их

Асинхронный является лучшим для этого тяжелого IO , связанных с переработкой , но как насчет тяжелой работы процессора обработки?

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

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

Аналогично, в многопроцессорном приложении задание может быть отправлено через обмен сообщениями (например, IPC, сокеты и т. Д.) В подпроцесс, специально предназначенный для обработки заданий.

На практике асинхронный и многопоточный / процессный код имеют свои преимущества и недостатки.

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

Примеры:

  • Хранилище (например, Amazon S3, Google Cloud Drive) связано с процессором
  • Веб-серверы связаны IO (Amazon EC2, Google App Engine)
  • Обе базы данных, привязанные к процессору для записи / индексации и ввода-вывода для чтения

Чтобы поместить это в перспективу ...

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

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

Лучшее приложение часто использует комбинацию обоих. Например, веб-приложение может использовать nginx (то есть асинхронный однопоточный) в качестве балансировщика нагрузки для управления потоком входящих запросов, аналогичный асинхронный веб-сервер (например, Node.js) для обработки http-запросов и набор многопоточных серверов. обрабатывать загрузку / потоковую передачу / кодирование контента и т. д.

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

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

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

Обновить:

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

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

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

Update2:

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

Эван Плейс
источник
почему программист должен идти многопроцессно? Я имею в виду, я предполагаю, что с более чем одним процессом вам также необходимо какое-то межпроцессное взаимодействие, которое может добавить значительные накладные расходы, это что-то вроде старого способа работы Windows-программиста? когда я должен перейти на несколько процессов? Кстати, спасибо за ваш ответ, действительно хорошее представление о том, для чего нужны асинхронные и многопоточные.
user1849534 15.12.12
1
Вы предполагаете, что межпроцессное взаимодействие увеличит общие накладные расходы. Однако, если состояние обработки является неизменным, или необходимо обрабатывать синхронизацию только при запуске / завершении. это может быть гораздо более эффективным, чтобы развернуть в более параллельные задачи. Образец актера - хороший пример, и если вы еще не читали об этом - его действительно стоит прочитать. akka.io
sylvanaar
1
@ user1849534 Несколько потоков могут общаться друг с другом через общую память + блокировку или IPC. Блокировка проще, но сложнее отлаживать, если вы допустили ошибку (например, пропустили блокировку, тупиковая блокировка). IPC лучше всего использовать, если у вас много рабочих потоков, потому что блокировка плохо масштабируется. В любом случае, если вы используете многопоточный подход, важно поддерживать абсолютный минимум связи / синхронизации между потоками (т. Е. Минимизировать накладные расходы).
Эван Плейс
1
@ akka.io Ты совершенно прав. Неизменность - это один из способов минимизировать / устранить накладные расходы на блокировку, но вы все равно несете временные затраты на переключение контекста. Если вы хотите расширить ответ, включив сведения о том, как неизменность может решить проблемы синхронизации потоков, не стесняйтесь. Основной момент, который я стремился проиллюстрировать, состоит в том, что существуют случаи, когда асинхронная связь имеет явное преимущество перед многопоточностью / процессом и наоборот.
Эван Плейс
(продолжение) Но, если честно, если бы мне понадобилось много возможностей обработки с привязкой к процессору, я бы пропустил модель актора и построил ее для возможности масштабирования до нескольких сетевых узлов. Лучшее решение, которое я видел для этого, это использование модели вентилятора задач 0MQ по коммуникациям на уровне сокетов. См. Рис. 5 @ zguide.zeromq.org/page:all .
Эван Плейс
13

Асинхронный подход Microsoft является хорошей заменой для наиболее распространенных целей многопоточного программирования: повышение скорости реагирования на задачи ввода-вывода.

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

Многопоточность для отзывчивости

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

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

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

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

«Асинхронная» альтернатива

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

Вместо создания нового потока, ожидающего получения сетевого ресурса (например, загрузки изображения), используется asyncметод, при котором awaitизображение становится доступным и, тем временем, уступает вызывающему методу.

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

Я не слишком углублялся в технические детали, но у меня сложилось впечатление, что управление загрузкой с периодической легкой загрузкой ЦП становится задачей не для отдельного потока, а скорее чем-то более похожим на задачу в очереди событий пользовательского интерфейса, и когда загрузка завершена, асинхронный метод возобновляется из этой очереди событий. Другими словами, awaitозначает что-то вроде «проверить, доступен ли мне нужный результат, если нет, вернуть меня в очередь задач этого потока».

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

Многопоточность для производительности

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

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

Тривиальный параллелизм

Конечно, иногда может быть легко получить реальное ускорение от многопоточности.

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

Практическая многопоточность для производительности

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

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

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

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

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

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

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

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

Параллельные алгоритмы

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

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

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

Теодор Мердок
источник
+1 за явно продуманный ответ. Однако я бы с осторожностью отнесся к предложениям Microsoft за чистую монету. Помните, что .NET - это синхронно-первая платформа, поэтому экосистема смещена в сторону предоставления более качественных средств / документации, которые поддерживают создание синхронных решений. Противоположное было бы верно для асинхронных первых платформ, таких как Node.js.
Эван Плейс
3

приложение не отвечает и просто медленно и неприятно.

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

Что касается «просто» асинхронного подхода, то это также многопоточность, хотя в большинстве сред она настроена для этого конкретного случая использования . В других эта асинхронность выполняется через сопрограммы, которые ... не всегда параллельны.

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

Telastyn
источник
Зачем ? Например, что вы нашли бананы в библиотеке boost сигналы2?
user1849534 15.12.12