Советы по преобразованию большого монолитного однопоточного приложения в многопоточную архитектуру? - 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 ]

1 голос
/ 05 февраля 2010
0 голосов
/ 28 декабря 2017

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

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

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

for each pixel in image:
    make it brighter

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

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

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

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

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

0 голосов
/ 04 июня 2010

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

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

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

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

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

  1. Вам нужен объект, который создается в главном потоке
  2. Объект копируется в нужную форму, только когда это необходимо и безопасным способом (хорошо, может потребоваться лучшая защита для растрового изображения, но для простоты ...)
  3. Объект 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 голосов
/ 18 февраля 2010

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

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

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

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

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

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

Я не уверен, но, возможно, libClient Toolset lib может помочь вам. На сайте автор написал:

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

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

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

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

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

НТН

...