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

31

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

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

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

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

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

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

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

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

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


Изменить: Я думал, что должен добавить еще пару подробностей о приложении:

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

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

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

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

  • 0
    Кто клиенты? Почему бы не разместить эту штуку в облаке Amazon и позволить клиентам запускать столько экземпляров, сколько они пожелают - по одному приложению на ОС. Если это займет часы - пусть будет так.
  • 0
    @Hamish: это настольное приложение. Приложение имеет лицензию на локальный запуск одного экземпляра и работает с большими наборами данных объемом в несколько гигабайт, размещенными на одном компьютере или в локальной сети. Если я что-то упустил (?), Я не уверен, что облачный / веб-подход является практическим решением. Перенос настольного приложения в облако кажется даже более трудоемким, чем многопоточность :)
Показать ещё 3 комментария
Теги:
architecture
multithreading
c++builder

15 ответов

14
Лучший ответ

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

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

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

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

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

  • 1
    Это интересный подход, о котором я не думал. Программа не всегда хорошо укомплектована - это старая кодовая база - но эта область, основанная на узлах, на самом деле является одной из самых чистых областей программы, поэтому ее стоит изучить. Спасибо!
  • 2
    Я от всей души одобряю этот подход. Он не только обеспечивает хорошую структуру, но и аккуратно выполнен, он сводит к минимуму конкуренцию за данные и необходимость в блокировке (таким образом максимизируя преимущества современных многоядерных процессоров и гиперпоточности). Ключевой трюк заключается в том, чтобы разрешить рабочим потокам только доступ к данным в объекте запроса и разместить свои результаты там же. Поскольку только один поток владеет запросом в данный момент времени, нет конфликта данных (кроме блокировок в очередях, чтобы сделать вставку / удаление атомарными).
Показать ещё 1 комментарий
15

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

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

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

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

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

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

  • 0
    +1 Мне нравится взломать. Не красиво, но это работает.
  • 2
    +1 обмен сообщениями также может быть выполнен с использованием некоторой очереди сообщений, например MSMQ
Показать ещё 2 комментария
7
  • Не пытайтесь многопоточать все в старом приложении. Многопоточность для того, чтобы сказать, что это многопоточность, - пустая трата времени и денег. Вы создаете приложение, которое что-то делает, а не памятник себе.
  • Профайл и изучите ваши потоки выполнения, чтобы выяснить, где приложение проводит большую часть своего времени. Профилировщик - отличный инструмент для этого, но так просто перешагивает код в отладчике. Вы найдете самые интересные вещи в случайных прогулках.
  • Отключите интерфейс от долговременных вычислений. Используйте методы связи с использованием сквозных потоков для отправки обновлений в пользовательский интерфейс из потока вычислений.
  • В качестве побочного эффекта №3: ​​внимательно подумайте о повторном включении: теперь, когда вычисление выполняется в фоновом режиме, и пользователь может облачиться в пользовательский интерфейс, что нужно сделать в пользовательском интерфейсе, чтобы предотвратить конфликты с фоном операция? Разрешить пользователю удалять набор данных во время работы над этими данными, вероятно, является плохой идеей. (Смягчение: вычисление делает локальный снимок данных). Имеет ли смысл, чтобы пользователь мог спутать несколько вычислительных операций одновременно? Если обработать хорошо, это может стать новой функцией и помочь рационализировать усилия по переделке приложений. Если игнорировать, это будет катастрофой.
  • Определите конкретные операции, которые кандидаты будут перенаправлены в фоновый поток. Идеальный кандидат обычно представляет собой единую функцию или класс, который выполняет большую работу (требуется "много времени" для завершения - более нескольких секунд) с четко определенными входами и выходами, которые не используют глобальных ресурсов и не делают не касайтесь пользовательского интерфейса напрямую. Оценить и расставить приоритеты кандидатов на основе того, сколько потребуется работы для модернизации этого идеала.
  • С точки зрения управления проектами, делайте шаг за шагом. Если у вас есть несколько операций, которые являются сильными кандидатами для перемещения в фоновый поток, и они не имеют взаимодействия друг с другом, они могут быть реализованы параллельно несколькими разработчиками. Тем не менее, было бы хорошим упражнением, чтобы все участвовали в одном преобразовании сначала, чтобы все понимали, что искать и устанавливать свои шаблоны для взаимодействия с пользовательским интерфейсом и т.д. Держите расширенную встречу с доской, чтобы обсудить дизайн и процесс извлечения одного функцию в фоновый поток. Идите, реализуйте это (вместе или обменивайтесь частями с отдельными лицами), затем снова соберитесь, чтобы собрать все вместе и обсудить открытия и точки боли.
  • Многопоточность - это головная боль и требует более тщательной мысли, чем прямое кодирование, но разделение приложения на несколько процессов создает гораздо больше головных болей, ИМО. Поддержка потоков и доступные примитивы хороши в Windows, возможно, лучше, чем некоторые другие платформы. Используйте их.
  • В общем, не делай больше, чем нужно. Легко сугубо над реализацией и более сложной проблемой, бросая в нее больше шаблонов и стандартных библиотек.
  • Если никто из вашей команды не выполнял многопоточную работу раньше, бюджетное время, чтобы сделать эксперта или средства нанять его в качестве консультанта.
  • 0
    Это выглядит очень хорошим советом, Дэнни - спасибо! Кстати, довольно круто получить от вас ответ - у меня есть копия Delphi Component Design на моем столе, когда я печатаю.
  • 0
    Привет! Я тоже ... Где-то под всем этим ...;>
Показать ещё 1 комментарий
6

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

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

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

В то же время...

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

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

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

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

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

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

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

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

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

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

6

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

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

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

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

  • 0
    Я собирался порекомендовать более или менее то же самое, за исключением того, что вместо «отдельных потоков» было бы проще реализовать «отдельные процессы» и избежать боли при проверке каждой отдельной функции C ++ на предмет безопасности потоков. Разбейте графический интерфейс, механизм вычислений и модель данных на три отдельных компонента, и они будут взаимодействовать посредством асинхронной передачи сообщений. Цель должна состоять в том, чтобы запустить более одного процесса механизма вычислений параллельно.
  • 0
    Когда ваш метод рисования ставит в очередь запрос, что он рисует, ожидая обработки запроса?
Показать ещё 2 комментария
3

Если бы это были мои доллары развития, которые я тратил, я бы начал с большой картины:

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

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

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

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

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

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

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

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

Уоррен

3

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

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

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

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

result = leftVal + rightVal; // [3]

Это довольно просто:

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

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

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

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

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

3

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

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

Вы не пользуетесь многоядерными архитектурами процессора, которые становятся настолько распространенными. Параллелизация позволяет разделить работу между несколькими ядрами. Вы можете написать этот код с помощью стандартных методов С++ для разделения и покорения, используя "функциональный" стиль программирования, где вы передаете работу для разделения потоков на этапе разделения. Google MapReduce шаблон является примером этой техники. У Intel есть новая библиотека CILK, чтобы предоставить вам поддержку компилятора С++ для таких методов.

Большая чувствительность GUI через асинхронный просмотр документов

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

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

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

  • 0
    Использование преимуществ многоядерных процессоров является одной из ключевых причин для этого. К сожалению, CILK выглядит так, как будто он не совместим с нашим компилятором (я только что добавил дополнительную информацию по этому вопросу в редакцию.) Re MVP: спасибо. Это хорошее резюме проекта, которое, я думаю, заслуживает изучения: я не думал, что модель будет уведомлять об этом вид. Наш нынешний дизайн совсем не MVP, я бы все равно хотел его изменить. Спасибо за рекомендацию книги тоже!
2

Вот что я буду делать...

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

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

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

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

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

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

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

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

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

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

-Rick

  • 3
    +1. Если бы вы могли просто профилировать свое приложение, найти несколько оптимизаций и повысить производительность на X% без изменения всей архитектуры, я бы обязательно сделал это первым!
  • 0
    Это не всегда так просто, особенно если код уже настроен, но всегда стоит сначала профилировать и понимать, что происходит.
Показать ещё 2 комментария
1

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

  • 0
    Вы не можете изменить время вычисления с минут на миллисекунды с помощью многопоточности, но вы, безусловно, можете улучшить этот размер в отзывчивости GUI, переместив медленные вычисления в другой поток. Поскольку Windows сериализует операции рисования внутри, вся обработка GUI должна выполняться из основного потока.
1
  • 1
    Это хорошая статья, однако техника, представленная в ней, не поможет решить самую большую проблему в приложении - синхронные вызовы, которые могут занять от миллисекунд до часов. Сокращение времени, которое занимает обработчик WM_PAINT с 4 часов до 1 часа на 4-ядерном компьютере, просто недостаточно. Расчеты должны выполняться асинхронно, и все пути кода для рисования и взаимодействия с пользователем должны выполняться не более чем за 100 миллисекунд.
0

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

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

То, что я имею в виду под однородными циклами, в основном является тем, что трансформирует данные очень простым способом, например:

for each pixel in image:
    make it brighter

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

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

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

Теперь, по общему признанию, я несколько уклонился от ваших вопросов, но большинство из них не нужно применять, если вы делаете то, что я предлагаю, по крайней мере, пока вы не проработали свой путь до такой степени, что больше думаете о многопоточности в отличие от просто распараллеливания деталей реализации. И вам, возможно, даже не нужно идти так далеко, чтобы иметь очень конкурентоспособный продукт с точки зрения производительности. Если у вас есть многообещающая работа в одном цикле, вы можете посвятить аппаратные ресурсы тому, чтобы этот цикл работал быстрее, а не одновременно запускал много операций. Если вам нужно прибегнуть к более асинхронным методам, например, если ваши горячие точки больше связаны с I/O, найдите подход async/wait, в котором вы запускаете задачу async, но делаете что-то еще, а затем ждите в async-задаче (-ах) завершить. Даже если это не совсем необходимо, идея состоит в том, чтобы разрезать изолированные области вашей кодовой базы, где вы можете, со 100% -ной уверенностью (или не менее 99.9999999%) сказать, что многопоточный код верен.

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

0

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

ШАГ 1 - Отзывчивый графический интерфейс

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

if (CurrenData.LastUpdate>CurrentUpdate)
    {
    Image1->Canvas->Draw(0,0,CurrenData.Bitmap);
    CurrentUpdate=Now();
    }

OK! Я знаю! Немного грязно, но это быстро и просто. Дело в том, что:

  • Вам нужен объект, созданный в основном потоке
  • Объект копируется в Форме, в которой вы нуждаетесь, только тогда, когда это необходимо и безопасным способом (нормально, может быть нужна лучшая защита для Bitmap, но для semplicity...)
  • Объект CurrentData - это ваш фактический проект, однопоточный, который создает изображение

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

ШАГ 2 - многопоточность

Я предлагаю вам реализовать класс следующим образом:

SimpleThread.h

typedef void (__closure *TThreadFunction)(void* Data);

class TSimpleThread : public TThread
{
public:
    TSimpleThread( TThreadFunction _Action,void* _Data = NULL, bool RunNow = true );
    void AbortThread();

    __property Terminated; 

protected:
    TThreadFunction ThreadFunction;
    void*           Data;

private:
    virtual void __fastcall Execute() { ThreadFunction(Data); };
};

SimpleThread.c

TSimpleThread::TSimpleThread( TThreadFunction _Action,void* _Data, bool RunNow)
             : TThread(true), // initialize suspended
               ThreadFunction(_Action), Data(_Data)
{
FreeOnTerminate = false;
if (RunNow) Resume();
}

void TSimpleThread::AbortThread()
{
Suspend(); // Can't kill a running thread
Free();    // Kills thread
}

Объясним. Теперь в вашем простом поточном классе вы можете создать такой объект:

TSimpleThread *ST;
ST=new TSimpleThread( RefreshFunction,NULL,true);
ST->Resume();

Давайте объясним лучше: теперь, в вашем собственном монолитическом классе, вы создали поток. Подробнее: вы добавляете функцию (т.е. RefreshFunction) в отдельный поток. Объем вашей функции одинаковый, класс один и тот же, исполнение является отдельным.

0

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

http://www.freevbcode.com/ShowCode.Asp?ID=1287

Надеюсь, что это поможет.

-3

Трудно дать вам правильные рекомендации. Но...

Самый простой способ - преобразовать ваше приложение в ActiveX EXE, поскольку COM поддерживает Threading и т.д., встроенный в него, ваша программа автоматически превратится в приложение Multi Threading. Конечно, вам придется внести немало изменений в свой код. Но это самый короткий и безопасный способ.

Я не уверен, но, возможно, RichClient Toolset lib может сделать трюк для вас. На сайте автор написал:

Он также предлагает бесплатную регистрацию Загрузка/Инстанс-возможности для ActiveX-Dll и нового, простого в использовании Threading-подхода, который работает с Named-Pipes под капюшона и работает поэтому кросс-процесс.

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

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

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

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

НТН

  • 0
    -1. Пожалуйста, объясните, как перемещение кода в библиотеки DLL сделает выполнение асинхронным и параллельным.
  • 1
    То, что мы сделали, инкапсулировало весь код, который будет выполняться для более чем одного события одновременно. В нашем случае это было, когда обновляется цена скрипта, что иногда одновременно для более чем одного скрипта. После разделения этого кода была создана библиотека ActiveX DLL и установлено самое важное свойство следующим образом: Instancing = MultiUser Теперь для каждого события обновления цены мы создадим новый экземпляр объекта ActiveX и передадим ему необходимые значения. Вот и все! Это автоматически создаст новые темы для каждого экземпляра, который мы создадим. В нашем случае мы никогда не должны быть уведомлены потоком.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню