Советы по преобразованию большого монолитного однопоточного приложения в многопоточную архитектуру? - PullRequest
32 голосов
/ 05 февраля 2010

Основной продукт моей компании - это большое монолитное приложение C ++, используемое для обработки и визуализации научных данных. Его кодовая база восходит, может быть, к 12 или 13 годам, и хотя мы приложили усилия для ее обновления и обслуживания (например, использование STL и Boost - когда я присоединился к большинству контейнеров - было на заказ - полностью обновлено до Unicode и VCL 2010 года и т. Д.) осталась одна очень важная проблема: она полностью однопоточная. Учитывая, что это программа обработки данных и визуализации, это становится все более и более помехой.

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

Поток данных программы может выглядеть примерно так:

  • окно должно рисовать данные
  • В методе рисования он вызывает метод GetData, часто сотни раз для сотен бит данных в одной операции рисования
  • Это будет идти и вычислять или считывать из файла или чего-либо еще требуемого (часто довольно сложный поток данных - представьте, что это данные, проходящие через сложный граф, каждый узел которого выполняет операции)

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

Я ищу совет о том, как подойти к изменению этого. Практические идеи. Возможно такие вещи, как:

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

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

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


Редактировать: Я подумал, что я должен добавить еще пару деталей о приложении:

  • Это 32-разрядное настольное приложение для Windows. Каждая копия лицензирована. Мы планируем сохранить настольное локально работающее приложение
  • Мы используем Embarcadero (ранее Borland) C ++ Builder 2010 для разработки. Это влияет на параллельные библиотеки, которые мы можем использовать, поскольку большинство (?) Написано только для GCC или MSVC. К счастью, они активно его развивают, и его поддержка стандартов C ++ намного лучше, чем раньше. Компилятор поддерживает эти компоненты Boost .
  • Его архитектура не так чиста, как должно быть, и компоненты часто слишком тесно связаны. Это еще одна проблема:)

Редактировать # 2: Спасибо за ответы до сих пор!

  • Я удивлен, что многие люди рекомендовали многопроцессорную архитектуру (сейчас это самый популярный ответ), а не многопоточность. У меня сложилось впечатление, что это очень сложная структура Unix-программы, и я ничего не знаю о том, как она разработана или работает. Есть ли хорошие ресурсы об этом в Windows? Это действительно так часто встречается в Windows?
  • С точки зрения конкретных подходов к некоторым предложениям о многопоточности, существуют ли шаблоны проектирования для асинхронного запроса и потребления данных, многопоточные или асинхронные системы MVP, или как проектировать систему, ориентированная на задачи, или статьи и книги и публикации - выпустить деконструкции, иллюстрирующие вещи, которые работают, и вещи, которые не работают? Конечно, мы можем сами разработать всю эту архитектуру, но хорошо работать с тем, что делали другие, и знать, каких ошибок и ошибок нужно избегать.
  • Один из аспектов, который не затрагивается ни в одном ответе, - это управление проектом. У меня сложилось впечатление, как долго это будет продолжаться, и я буду хорошо контролировать проект, когда буду делать что-то настолько неопределенное, насколько это может быть сложно. Думаю, это одна из причин, по которой я следую рецептам или практическим советам по кодированию, чтобы как можно больше руководить и ограничивать направление кодирования.

Я еще не пометил ответ на этот вопрос - это не из-за качества ответов, что замечательно (и спасибо), а просто потому, что из-за объема этого я надеюсь получить больше ответов или обсуждение. Спасибо тем, кто уже ответил!

Ответы [ 15 ]

16 голосов
/ 05 февраля 2010

У вас впереди большой вызов. Передо мной стояла аналогичная задача - 15-летняя монолитная однопоточная кодовая база, не использующая преимущества многоядерности и т. Д. Мы приложили немало усилий, чтобы найти дизайн и решение, которое было бы работоспособным и работало бы.

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

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

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

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

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

15 голосов
/ 05 февраля 2010

Итак, в вашем описании алгоритма есть подсказка о том, как действовать:

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

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

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

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

8 голосов
/ 04 июня 2010
  1. Не пытайтесь многопоточности в старом приложении. Многопоточность ради того, чтобы сказать, что она многопоточная - это пустая трата времени и денег. Вы создаете приложение, которое делает что-то, а не памятник себе.
  2. Профилируйте и изучайте свои потоки выполнения, чтобы выяснить, где приложение проводит большую часть своего времени. Профилировщик - отличный инструмент для этого, но он просто шагает по коду в отладчике. Вы находите самые интересные вещи в случайных прогулках.
  3. Отделение пользовательского интерфейса от длительных вычислений. Используйте методы связи между потоками для отправки обновлений в пользовательский интерфейс из потока вычислений.
  4. В качестве побочного эффекта # 3: внимательно подумайте о повторном входе: теперь, когда вычисления выполняются в фоновом режиме, и пользователь может покопаться в пользовательском интерфейсе, какие функции в пользовательском интерфейсе следует отключить, чтобы предотвратить конфликты с фоном операция? Разрешение пользователю удалять набор данных во время выполнения вычислений с этими данными, вероятно, является плохой идеей. (Смягчение: вычисления делают локальный снимок данных) Имеет ли смысл для пользователя одновременно распределять несколько вычислительных операций? При правильном обращении это может стать новой функцией и помочь рационализировать усилия по переработке приложения. Если игнорировать это будет катастрофа.
  5. Определите конкретные операции, которые являются кандидатами для добавления в фоновый поток. Идеальным кандидатом, как правило, является отдельная функция или класс, выполняющий большую работу (требующий «много времени» для выполнения - более нескольких секунд) с четко определенными входами и выходами, который не использует глобальные ресурсы и выполняет не трогать пользовательский интерфейс напрямую. Оценивайте и расставляйте приоритеты кандидатов на основе того, сколько работы потребуется для адаптации к этому идеалу.
  6. С точки зрения управления проектами, делайте шаг за шагом. Если у вас есть несколько операций, которые являются сильными кандидатами для перемещения в фоновый поток, и они не взаимодействуют друг с другом, они могут быть реализованы параллельно несколькими разработчиками. Тем не менее, было бы неплохо, чтобы все сначала участвовали в одном преобразовании, чтобы все понимали, на что обращать внимание, а также для определения шаблонов взаимодействия с пользовательским интерфейсом и т. Д. Проведите расширенное собрание на доске, чтобы обсудить дизайн и процесс его извлечения функция в фоновом потоке. Примените это (вместе или раздайте кусочки отдельным лицам), затем соберите все вместе, чтобы обсудить открытия и болевые точки.
  7. Многопоточность является головной болью и требует более тщательного анализа, чем прямое кодирование, но разбиение приложения на несколько процессов создает гораздо больше головной боли, IMO. Поддержка потоков и доступные примитивы хороши в Windows, возможно, лучше, чем на некоторых других платформах. Используйте их.
  8. В общем, не делайте больше, чем нужно. Реализовать и чрезмерно усложнить проблему легко, добавив больше шаблонов и стандартных библиотек.
  9. Если никто из вашей команды не выполнял многопоточную работу раньше, выделите время, чтобы найти эксперта или средства, чтобы нанять его в качестве консультанта.
7 голосов
/ 05 февраля 2010

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

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

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

Тем временем ...

У вас должно быть 2 полных копии этой готовой к показу структуры. Одним из них является то, на что смотрит сообщение WM_PAINT. (назовите это cfd_A ) Другое - это то, что вы передаете своей функции CookDataForDisplay (). (назовите это cfd_B ). Ваша функция CookDataForDisplay () выполняется в отдельном потоке и работает над сборкой / обновлением cfd_B в фоновом режиме. Эта функция может занять столько времени, сколько ей нужно, потому что она никак не взаимодействует с дисплеем. Как только вызов вернется, cfd_B будет самой последней версией структуры.

Теперь поменяйте местами cfd_A и cfd_B и InvalidateRect в окне вашего приложения.

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

Итак, вернемся к вашему примеру.

  • В методе рисования он вызывает метод GetData, часто сотни раз для сотен бит данных в одной операции рисования

Теперь это 2 потока, метод рисования ссылается на cfd_A и выполняется в потоке пользовательского интерфейса. Тем временем cfd_B создается фоновым потоком с использованием вызовов GetData.

Быстрый и грязный способ сделать это

  1. Возьмите ваш текущий код WM_PAINT, вставьте его в функцию под названием PaintIntoBitmap ().
  2. Создайте растровое изображение и DC памяти, это cfd_B.
  3. Создать поток и передать его cfd_B и вызвать его PaintIntoBitmap ()
  4. Когда этот поток завершится, поменяйте местами cfd_B и cfd_A

Теперь ваш новый метод WM_PAINT просто берет предварительно обработанный растровый рисунок в cfd_A и выводит его на экран. Ваш пользовательский интерфейс теперь отключен от вашей внутренней функции GetData ().

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

6 голосов
/ 05 февраля 2010

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

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

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

Это сохранит ваш пользовательский интерфейс быстрым без значительных трудностей многопоточности ваших рабочих процедур (что может быть похоже на полное переписывание)

3 голосов
/ 09 февраля 2010

Если бы я тратил свои доллары на разработку, я бы начал с общей картины:

  1. Чего я надеюсь достичь, и сколько я потрачу на это, и как я буду дальше? (Если ответом будет то, что мое приложение будет работать на 10% лучше на четырехъядерных ПК, и я мог бы достичь того же результата, потратив на 1000 долларов больше на клиентский ПК и потратив на 10000 долларов меньше в этом году на исследования и разработки, тогда я бы пропустил все усилия).

  2. Почему я делаю многопоточное, а не массово параллельное распределение? Я действительно думаю, что потоки лучше, чем процессы? Многоядерные системы также хорошо работают с распределенными приложениями. И у систем на основе процессов передачи сообщений есть некоторые преимущества, которые выходят за рамки преимуществ (и затрат!) Многопоточности. Должен ли я рассмотреть процессный подход? Должен ли я считать фон, работающий полностью как сервис, и графический интерфейс переднего плана? Так как мой продукт заблокирован и лицензирован, я думаю, что услуги (поставщик) мне вполне подойдут. Кроме того, разделение элементов на два процесса (фоновый сервис и передний план) может привести к тому, что произойдет такой вид переписывания и реархитектуры, который я не буду вынужден делать, если буду просто добавлять потоки в свой микс.

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

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

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

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

Интересно, похожи ли проблемы с «дизайном» этого приложения на C ++ Builder на мою болезнь Delphi «RAD Spaghetti». Я обнаружил, что оптовый рефакторинг / перезапись (более года для каждого основного приложения, для которого я это сделал) - это минимальное время, необходимое для того, чтобы разобраться с приложением «случайная сложность». И это было без использования идеи «где возможно». Я склонен писать свои приложения с потоками только для последовательной связи и обработки сетевых сокетов. И, может быть, странная «очередь-поток-очередь».

Если в вашем приложении есть место, где вы можете добавить ОДИН поток, чтобы протестировать воды, я бы искал основную «рабочую очередь» и создал бы экспериментальную ветку контроля версий, и я бы узнал, как работает мой код работает, разбивая его в экспериментальной ветви. Добавьте эту тему. И посмотрите, где вы проведете свой первый день отладки. Тогда я мог бы просто оставить эту ветку и вернуться к своему стволу, пока боль в моей височной доле не утихнет.

Уоррен

3 голосов
/ 05 февраля 2010

Есть кое-что, о чем еще никто не говорил, но это довольно интересно.

Это называется future с. Будущее - это обещание результата ... посмотрим на примере.

future<int> leftVal = computeLeftValue(treeNode); // [1]

int rightVal = computeRightValue(treeNode); // [2]

result = leftVal + rightVal; // [3]

Все довольно просто:

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

  2. Пока вычисляется leftVal, вы вычисляете rightVal.

  3. Вы добавляете два, это может блокироваться, если leftVal еще не вычислено, и ждать окончания вычисления.

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

См. статью Херба Саттера о future s, они будут доступны в следующем C++0x, но сегодня уже есть библиотеки, даже если синтаксис, возможно, не такой красивый, как я бы вас верю;)

3 голосов
/ 05 февраля 2010

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

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

Вы не используете преимущества многоядерных архитектур ЦП, которые становятся настолько распространенными. Распараллеливание позволяет вам распределять работу между несколькими ядрами. Вы можете написать этот код с помощью стандартных методов C ++ «разделяй и властвуй», используя «функциональный» стиль программирования, когда вы передаете работу отдельным потокам на стадии разделения. Шаблон Google MapReduce является примером этой техники. Intel имеет новую библиотеку CILK , которая предоставит вам поддержку компилятора C ++ для таких методов.

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

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

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

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

2 голосов
/ 05 февраля 2010

Вот что я бы сделал ...

Я бы начал с профилирования и увидел:

1) что медленно и что такое горячие пути 2) какие вызовы являются реентерабельными или глубоко вложенными

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

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

Я бы использовал хороший системный профилировщик и хороший профилировщик выборки (например, инструментарий windows perforamnce или параллельные представления профилировщика в Visual Studio 2010 Beta2 - сейчас они оба «бесплатны»).

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

Если у вас нет такого хорошего инструмента рефакторинга, как VisualAssist, инвестируйте в него - оно того стоит. Если вы не знакомы с книгами по рефакторингу Майкла Фезерса или Кента Бека, подумайте о том, чтобы одолжить их. Я хотел бы убедиться, что мои рефакторинги хорошо охвачены модульными тестами.

Вы не можете перейти на VS (я бы порекомендовал продукты, которые я работаю с библиотекой асинхронных агентов и библиотекой параллельных шаблонов, вы также можете использовать TBB или OpenMP).

В boost я бы внимательно посмотрел на boost :: thread, библиотеку asio и библиотеку сигналов.

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

-Rick

1 голос
/ 18 февраля 2010

Ну, я думаю, вы ожидаете многого, основываясь на ваших комментариях здесь. Вы не собираетесь переходить от минут к миллисекундам с помощью многопоточности. Максимум, на что вы можете надеяться, это текущее количество времени, деленное на количество ядер. При этом вам немного повезло с C ++. Я написал высокопроизводительные мультипроцессорные научные приложения, и вы хотите найти самый параллельный цикл , который вы можете найти. В моем научном коде самая тяжелая часть вычисляет где-то между 100 и 1000 точками данных. Тем не менее, все точки данных могут быть рассчитаны независимо от других. Затем вы можете разделить цикл, используя openmp. Это самый простой и эффективный способ. Если ваш компилятор не поддерживает openmp, то вам будет очень сложно портировать существующий код. С openmp (если вам повезет), вам может понадобиться всего лишь добавить пару #pragmas, чтобы увеличить производительность в 4-8 раз. Вот пример StochFit

...