" title="Написать письмо">Написать письмо
Донаты на карту ВТБ:
2200 4002 2461 6363

Статистика

Пользователи : 1
Статьи : 2357
Просмотры материалов : 8952986
 
Многопоточность: получилось-4 (25.09.2025). Печать E-mail
2025 - Сентябрь
25.09.2025 13:01
Save & Share
Flawless victory с элементами fatality - так можно оценить исследовательскую работу по многопоточности. Сначала многопоточность успешно была реализована в Qt, потом - в Borland C++ Builder v.6.0, далее - реализовано её самое сложное подмножество: параллельное программирование с использованием реальных данных большого объёма. Теперь нужно объединить содержимое прошлых материалов, с исправлением ошибок, - и выдать некую базу, абсолютно точно являющуюся реальностью.


Цель материала и причины его написания (накладывание серьёзных проблем друг на друга):
- путаница в определениях многопоточности, её свойств и подмножеств - у множества людей, включая авторов книг. Многопоточность в РФ, вообще, мало распространена - и мало специалистов, которые грамотно могут всё это объяснить;
- подавляющая неоднозначность понимания протекающих процессов ("суслика видишь? Вот и я не вижу. А он есть..."). Невидимые напрямую абстракции - которые можно проверить только опытами и косвенными результатами (осложняемые большими объёмами данных);
- информации как в интернете, так и в книгах, - недостаточно для понимания (в т.ч. из-за корявого описания). В части книг по C++ - о многопоточности вообще ни слова (на примере Федоренко 2010 года и Липпмана неизвестного года - и последний "считается одним из лучших учебников по C++", по мнению ИИ гугла). А на форумах - практически поголовно написаны неработающие в реальности примеры. Был даже прикол: ТС задал вопрос, ему ответили, он подумал "ну нафиг, хрень какая-то" и свалил - и ветка осталась в сраче ответчиков друг с другом без решения вопроса ТС - а потом и ТС опустили;
- разная работоспособность (полная/частичная/никакая - любой из вариантов) параллелизма и его обслуживающих функций (а также время его выполнения): на разных процессорах, разных ОС, разных языках программирования, разных версиях языков программирования. Имеет даже значение, в реальной ОС работают потоки или в виртуальной машине;
- по итогу, заявить о знании параллельного программирования можно только тогда, когда ты успешно его отработал на 2 языках программирования, во множестве ОС - написав 3 версии одной и той же программы: однопоточную и 2 многопоточных (значит, будет ещё и 5-й материал по Qt когда-нибудь). Об обычной многопоточности - сейчас это можно сказать, а параллелизм - реализован только на Borland C++ Builder v.6.0. Собственно, это развенчание мифа в интернете, что старый язык 2002 года не умеет работать нормально с потоками: получилось всё. Или почти всё. Или почти получилось. Сами решите потом, как получилось, - вот самая простая неоднозначность из множества;
- а значит, что подавать материал по многопоточности - нужно как молоко для младенца: медленно и дозированно, начиная от выставления руки горизонтально для расстёгивания блузки. И даже при таком раскладе - будут тонны переписанных по несколько раз исходников, куча потраченных времени и нервов - а также некоторое разочарование даже в конечном положительном результате;
- если в ТТХ ЦП 12 потоков - это не значит, что используя 11 потоков (1-й - родительский, интерфейсом занят) - скорость будет увеличена в 11 раз. По факту, используя 11 потоков место 1, - удалось добиться повышения скорости в 3.2 раза. Также использование 11 потоков - не значит их загрузку на 100% хотя бы временно. Удалось добиться максимальной и постоянной загрузки ЦП на 50% - 23.3% из которых уходит на обслуживание созданного параллелизма. И это не ошибка самоучки: и в других продуктах (на примере Qt) написанные разработчиками алгоритмы несовершенны;
- если в ТТХ ЦП указаны потоки как неизменная реальная характеристика реального объекта - то при программировании создаются потоки цифровые - и можно в 1 реальный поток разместить несколько цифровых. И будут они мешать друг другу, работая медленно и разрывая частоту реального потока на куски;
- а как много больше RAM при параллельном программировании требуется (ведь одновременно работают несколько функций) - можно в архиваторе 7-Zip посмотреть, увеличивая количество потоков до нажатия кнопки процесса архивирования: и за 500ГБ можно выйти;
- окончательным итогом - становится предостережение: если босс-гуманитарий ставит задачу переделать однопоточное приложение в многопоточное (и, как правило, самое сложное: параллелизм - чтобы быстрее работало) - нужно сразу напихать ему что-то там за воротник, потребовав очень большие сроки для переработки приложения и не гарантируя результата. Можно, не осознавая ошибок до самого конца, так напрограммировать - что продукт начнёт медленнее работать, чем ранее в 1 потоке.

Чтобы упростить всё, выкидываются все свойства и подмножества многопоточности. Много потоков и работа с ними в разных ситуациях - и только это: игры в эрудита со всякими синхронностям отменяются. И сначала надо разобраться в многопоточности как сущности:
- один поток ЦП всегда равен половине частоты ядра. Это видно в диспетчере задач при создании нагрузки на приложение при помощи while (true). Ядро ЦП может разгоняться по технологиям создателей, увеличивая и частоту потока, - но здесь не всё гладко (особенно в Astra Linux);
- однопоточное приложение, физически, всегда занимает один поток ЦП. Поток при этом, может быть под совершенно любым номером, зависит от ОС (ProcessLasso - вообще позволяет перекидывать программы на фиксированные потоки и удерживать их там);
- цель многопоточности - получение доступа программы к другим реальным потокам ЦП: тогда можно и их вычислительную мощность использовать, и избавить интерфейс от зависания во время работы.

Нахождение/создание работоспособного исходного кода (класс потока и создание объекта потока) - первая из нетривиальных задач. На примере Borland C++ Builder v.6.0:
- создание класса потока и функции потока Execute(): никаких примеров из интернета - пользоваться только средой разработки. File→New→Other...→прокрутить→Thread Object". В новых CPP- и H- файлах будет нужная информация + будет список необходимых инклудов. Для каждого потока создаётся отдельный файл - но теперь, когда есть образец класса и функции потока, можно сохранить их оптимизированный вручную пустой вариант и быстро доставать при необходимости из личного репозитория (в данном случае - назвал cThread). Если бы это была более новая версия Borland, от Embarcadero, - алгоритм создания был бы другой, в Qt - тоже другой. Возможно, это самый сложный из этапов создания потока в условиях неопределённости;
- в созданный класс потока можно дописывать свои плюшки, облегчающие работу с ним. Например, параметр, хранящий чёткий номер порции данных - из кучи данных для множества потоков;
- после создания класса потока - проблема не закончилась: требуется в месте создания объектов потоков написать "__fastcall cThread::cThread(bool CreateSuspended) : TThread(CreateSuspended){}". Эта строчка создаётся при создании класса - просто она отдельно от класса и функции. И про неё надо не забыть, хранить вместе с классом потока в репозитории;
- создание объекта потока: сThread *oThread = new cThread(true);, где true - не запускать функцию Execute потока автоматом, false - запускать. Без new - нельзя (и живите теперь с этим). А раз всегда new - значит, нужен delete для предотвращения утечек памяти. А delete и Terminate потока - не работают должным образом вне потока, вплоть до краха программы. Выход - в функции потока разместить: в начале написать "FreeOnTerminate = true;" (и эту строчку можно найти лишь в 1-2 темах по всему интернету - недокументированная особенность), а в конце - "cThread::Terminate();";
- у потока есть ещё недокументированные особенности. Не работают должным образом функции WaitFor (подождать без загрузки ЦП, пока поток закончит работу), Suspend (свойство Suspended остаётся false), свойство Suspended (всегда false). Контролировать процессы выполнения потока приходится через глобальные переменные в while (true). При этом, если разные потоки одновременно стучатся в разные части массива, - они их не портят, т.к. не пересекаются байтами RAM, в которых меняют информацию;
- обязательная проверка созданного потока в кнопке: while (true) - загрузит поток на 100%, и это отобразится в диспетчере задач (~8.33% при 6 ядрах). При этом, интерфейс программы висеть не будет и без Application->ProcessMessages(). Если нажимать кнопку создания потока снова и снова - всё больше и больше ЦП будет загружаться, пока не достигнет отметки 100% (здесь интерфейс начнёт уже лагать - и Windows тоже начнёт лагать с долей вероятности, т.к. последний реальный поток будет занят цифровым);
- вот сколько текста написано в виде талмуда - а только один объект потока был создан и наполнен необходимыми строками, а присущие ему исходники пустыми сохранены в репозитории. А когда нет на руках такой инструкции - эти строки выстрадываются часами и днями, перемалывая в интернете просто тонны информации и много неработающих примеров. И это всё - только по одной среде программирования, одной её версии. И ведь не всё однозначно: вдруг, существуют ещё какие-то строчки недокументированные - чтобы заработала, например, WaitFor.

А вот как заставить работать множество потоков не просто одновременно, а с большой кучей общих данных, - ещё один геморрой вселенского масштаба:
- скорость ЦП не фиксированная и постоянно плавает - и у потока тоже плавает. Значит, 2 одновременно запущенных потока, даже без работы с данными и с абсолютно одинаковым кодом, - закончат работу либо одновременно, либо первый будет раньше, либо первый будет позже. В итоге, когда куча потоков работают одновременно - это хаос без возможности предсказания, когда какой поток закончит работу. Значит, в случае записи каких-то общих данных в массив, - сначала будет записано число 3, потом число 1, потом число 2 - вместо 1-2-3. Решение вопроса - создание в классе потока переменной, отвечающей за номер порции данных перед запуском потока. Цифры по-прежнему вернутся по скорости как 3-1-2 - но каждая из них будет положена в конкретную заранее заданную ячейку;
- если за потоками не следить - они начинают портить данные, которыми совместно пользуются. В интернете это показывается на примере записи в файл, ещё как-то - но сложно для понимания. Самый простой способ увидеть засирание данных - создать 11 потоков, внутри потока глобальной переменной g_iCall_Execute_Count единицу прибавлять. И тут - главное не промахнуться: у Arduino есть префикс volatile, приписываемый к переменным, которые должны изменяться очень быстро, - без него и в билдере будет получен неправильный результат. Теперь: как только 2 потока совпадут друг с другом в прибавлении единицы к переменной - они оба возьмут текущее число. То есть, условно, вместо 34-35-36 у 2 потоков - получится 34-34-35. Переменная всегда будет меньше числа вызова функции Execute - при этом, каждый раз будет получено другое число;
- чтобы потоки не мешали друг другу (как в пункте выше), придумали блокировку: потоки терпеливо ждут, пока один поток закончит работать, определится следующий в очереди - и процесс циклично повторяется. А ожидание означает задержки в работе. В итоге, заблокировал потоки таким образом - что время выполнения увеличилось на ~20%. Блокировку нужно проводить внутри функции Execute - только в тех местах, где в общие данные запись ведётся: минимальное количество строк;
- в интернете верещат, что потоки всегда нужно блокировать. Но, если потоки используют общий массив/структуру и пишут данные в её разные части, - блокировка потоков вообще не нужна (доказано на огромном объёме данных) - время выполнения сэкономить можно;
- для данной среды программирования существуют 3 вида блокировки потоков: Synchronize, критические секции и мьютексы (ну почему мьютексы, а не мутексы - бвееээ). С Synchronize не надо даже пытаться что-то сделать - забыть как страшный сон: сложная + считается самым устаревшим и медленным методом из 3 (а это точно правда? - не проверялось). Мьютексы требуют Handle как дополнительный параметр, в отличие от критических секций, - а также работают медленнее (не проверялось: кто сказал?). Поэтому были выбраны критические секции.

Чтобы заставить критическую секцию работать - нужно сделать очень точные ритуалы (и опять, примеры из интернета будут максимально пытаться сбить с толку - и сами вы будете сбивать себя с толку человеческим фактором, откатываясь в прогрессе назад. "Со смертью этого персонажа нить вашей судьбы обрывается. Загрузите сохранённую игру дабы восстановить течение судьбы, или живите дальше в проклятом мире, который сами и создали"):
- глобальная переменная подсчёта вызова Execute: volatile int g_iThreads_Counter = 0;: обязательный самоконтроль создаваемой секции - иначе не будет понятно, работает ли она;
- глобальная переменная секции: CRITICAL_SECTION g_csCThread. Одна секция - для одной функции потока. Несколько разных функций Execute - несколько критических секций;
- создание критической секции InitializeCriticalSection(&g_csCThread); - в любом месте до вызова функции Execute потока (но ни в коем случае в самом потоке);
- в самом потоке определить защищаемый от искажения данных код: заключение его между строками EnterCriticalSection(&g_csCThread); и LeaveCriticalSection(&g_csCThread);;
- когда работа с потоками закончена окончательно: DeleteCriticalSection(&g_csCThread); - убить критическую секцию;
- сравнить значение переменной g_iThreads_Counter с фактическим количеством запуска Execute.

Правильная загрузка всех потоков данными:
- на данный момент существует процессор со 128 потоками (64 ядра) - максимальное их количество. Создаётся глобальный массив для принудительной нумерации порций данных для потоков: volatile unsigned __int64 g_ui64Threads_Params[128][3];. В 3 - входит входная информация, флаг обработки потока (не взял в работу, взял в работу, окончил работу), выходная информация (естественно, всё занулить перед использованием). Массив потоков cThread *g_oThreads[128]; (а тут - без new, внезапно) создавать смысла не оказалось: ни контроля потоков нет, ни скорости не прибавило;
- unsigned int g_uiProcessors_Number = 0;: количество реальных потоков в системе (windows.h: SYSTEM_INFO sysInfo; GetSystemInfo(&sysInfo); g_uiProcessors_Number = sysInfo.dwNumberOfProcessors;). Глючит в Windows XP и виртуальной машине - в Windows 7 и поздних ОС Windows работает исправно. Если возвращает 1 - нужно принудительно исправить на 2: 2 цифровых потока корректно засунутся в 2 реальных (если реальных, по факту, больше) или просто будут рвать 1 поток на части (что может выражаться в некотором зависании интерфейса);
- создание потока и его запуск: g_oThreads[j] = new cThread(true); g_oThreads[j]->iThreads_ParamsNumber = j; g_oThreads[j]->Resume();;
- в интерфейсном (первом) потоке: войти в ожидание while (true) и ждать окончания работы какого-то из потоков в for (параллельно ведя и счёт оконченных). Закончил - выходные данные по-тихому переместить, пока остальные потоки работают. Все потоки завершились - новые порции данных приготовить и создать объекты потоков снова.

При работе с параллелизмом - требуются средства дебага. В программе они выражены так:

Дебаг.
Для вычисления времени выполнения определенных циклов.
1 - функция кнопки (общее время работы), 2 - функция потока, 3 - суммарная работа потоков, 4 - формирование текстового массива и запись в файл, 5 - запись в Memo,
6 - FormCreate, 7 - анализ числа на простоту.
*/
LARGE_INTEGER g_liBegin_Time[7], g_liEnd_Time[7];
LARGE_INTEGER g_liFrequency; //Частота процессора.
const bool g_bDebug = true; //false - оптимизация по скорости.

volatile int g_iThreads_Counter = 0; //Корректный подсчёт количества запущенных потоков - видна именно правильная работа критической секции.

double dTime_Execution_Debug(int iLevel){return 1000 * (double)(g_liEnd_Time[iLevel].QuadPart - g_liBegin_Time[iLevel].QuadPart) / g_liFrequency.QuadPart;} //iLevel - индекс массива.

Нерешённая (а, может, и нерешаемая) задача - загрузить ЦП на 100%: первый поток вызывает торможение других потоков - до абсурда: с ProcessMessages работает в десятки-сотни раз быстрее, чем без неё, - и это не связано с использованием глобальных переменных или стека. Остаётся только попробовать изменить приоритет потоков, кроме первого или только первого, на максимум (с помощью свойства Priority = tpTimeCritical;) - но пока страшно.

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

(добавлено 26.09.2025) Ещё тонкости:
- многопоточное приложение становится менее надёжным, чем однопоточное. На примере с приоритетом потока: выставление tpTimeCritical - дало прирост в скорости в 2 раза в Windows XP в виртуальной машине, но не дало никакого прироста в современных ОС (а заодно породило и баг: при окончании работы - критическая ошибка 0 (лучше не придумаешь номер) в Borlndmm.dll, что не даёт программе корректно продолжать работу);
- реальный поток ЦП - называется "логический процессор";
- в Windows 10 среднее значение в диспетчере задач можно разобрать, изменив вид на отображение загрузки каждого потока. К сожалению, происходит то же, что и в многопоточности Qt: ОС сама решает, насколько сильно ей грузить конкретный поток - и не видно ни 1 потока, постоянно загруженного на 100%. В интернете не советуют конкретно второму логическому процессору присваивать конкретно второй поток - но если бы это удалось сделать, можно было бы однозначно трактовать: какой поток что делает, и как долго, и без перераспределения нагрузки между потоками.

(добавлено 29.09.2025) Ещё одно доказательство, что Visual Basic v.6.0 оптимальнее по ресурсам, чем Borland C++ Builder v.6.0. Однопоточная версия программы на бейсике ест всего 1.8МБ, многопоточная на борланде - 143.2МБ.

(добавлено 30.09.2025) Ложка дёгтя. Работа на 24-потоковом ЦП E5-2696 V2 - всего 11% от ЦП. В предыдущем материале - удавалось выжать 20%. Что изменилось - непонятно. Соответственно, прирост скорости относительно однопоточного варианта - всего в 2.66 раза.

Возможно, Builder, действительно, плохо работает с потоками: как нерабопоспособность WaitFor. Единственный способ это узнать - переписать программу под Qt и оценить. Однако, доказанная многопоточность - в Qt 5.15.2 (только под Linux), а под Windows - есть только Qt v.5.5.1. А значит, её ещё на многопоточность тестировать надо - а она работает только в Windows XP, как Borland.
Обновлено ( 09.12.2025 10:56 )
 
 

Последние новости


©2008-2026. All Rights Reserved. Разработчик - " title="Сергей Белов">Сергей Белов. Материалы сайта предоставляются по принципу "как есть". Автор не несет никакой ответственности и не гарантирует отсутствие неправильных сведений и ошибок. Вся ответственность за использование материалов лежит полностью на читателях. Размещение материалов данного сайта на иных сайтах запрещено без указания активной ссылки на данный сайт-первоисточник (ГК РФ: ст.1259 п.1 + ст.1274 п.1-3).

Много статей не имеет срока устаревания. Есть смысл смотреть и 2011, и даже 2008 год. Политика сайта: написать статью, а потом обновлять ее много лет.
Рекламодателям! Перестаньте спамить мне на почту с предложениями о размещении рекламы на этом сайте. Я никогда спамером/рекламщиком не был и не буду!
Top.Mail.Ru