Конференция "Основная" » Снова многопоточность
 
  • Denchik (17.08.17 16:54) [0]
    Доброго времени суток! Уважаемы знатоки, ищу изящное решение вопроса.

    Задача: максимально быстро просчитать md5 списка фалов. Идея, раскидывать список файлов на потоки, чтобы каждый обсчитывал свой кусок списка. Количество потоков = количеству ядер CPU в системе.

    Создание и старт и ожидание потоков:


    const
     MAXFILESIZE=50*1024*1024; // 50mb
    var
     ThrCnt: integer;
     FilesSzPerThread,
     CurThrFilesSz,
     TotalFilesSz,
     TotAllThrFilesSz,
     CurAllThrFilesSz: int64;

     ThrList: array of TThreadMultiMd5;
     AFileHashesItem: TFileHashesItem;
     i, j: integer;
     rWait: cardinal;
     ThrHandles: array[0..MAXIMUM_WAIT_OBJECTS - 1] of THandle;
     MalwProgressData: TMalwProgressData;
    begin
     ThrCnt:=CPUCount; // опеределяю сколько ядер

     TotalFilesSz:=FFileHashesList.GetTotalFilesSize(MAXFILESIZE);
     FilesSzPerThread:=Round(TotalFilesSz/ThrCnt); // Считаю какой размер файлов будет приходиться на каждый поток
     SetLength(ThrList, ThrCnt);

     for i := 0 to ThrCnt-1 do
     begin
       ThrList[i]:=TThreadMultiMd5.Create(True, FRHashFileProcPtr);
       ThrHandles[i]:=ThrList[i].Handle;

       // тут набиваю списки потоков именами файлов для которых нужно посчитать md5

       {$IFDEF LOGMODE}WrLog(Self.ClassName+' - CalcFilesMd5MultiThread, Thread #'+IntToStr(i)+' initialized, scheduled files size: '+GetHumanFileSize(CurThrFilesSz)+', total thread files: '+IntToStr(ThrList[i].FileHashesList.Count));{$ENDIF}

       ThrList[i].Start;
     end;

     {$IFDEF LOGMODE}WrLog(Self.ClassName+' - CalcFilesMd5MultiThread, Threads started');{$ENDIF}

     // жду когда потоки завершат свою работу, по пути обновляю юзеру прогресс бар
     repeat
       rWait:=WaitForMultipleObjects(ThrCnt, @ThrHandles, True, 300); // 300ms, scan progress refreshing interval

       // тут собственно обновление самого прогресса

     until (rWait<>WAIT_TIMEOUT) and (rWait <> (WAIT_OBJECT_0 + ThrCnt));




    Поток который занимается непосредственно подсчётом md5:


    .....

    procedure TThreadMultiMd5.Execute;
    begin
     {$IFDEF LOGMODE}WrLog(Self.ClassName+' - Starting thread with handle: '+IntToStr(Handle));{$ENDIF}
     // обсчитываю md5
     ...



    Запускаю всю эту лавочку и вижу в логах, что на момент когда дёргается функция WaitForMultipleObjects() ещё не все потоки успели запустится, а которые успели запустится уже и успели отработать (случай когда мало файлов и много ядер CPU). WaitForMultipleObjects выскакивает не дождавшись тех потоков которые не успели запуститься. Попробовал костыль в виде Sleep(2000); перед циклом ожидания завершения потоков. Всё в порядке. Но это костыль + в задаче критична каждая секунда ожидания.

    Собственно вопрос, каким образом грамотно понять что все потоки запустились и можно переходить к циклу с WaitForMultipleObjects ?

    С многопоточностью дел имел мало и первое что приходит в голову, в каждом потоке организовать какой нибудь доп. мьютекс, который будет заниматься в OnCreate каждого нового потока и освобождаться в OnExecute. Далее, в том месте где я запускаю потоки, ждать пока все эти мьютексы всех потоков не освободятся и уже потом запускать цикл с WaitForMultipleObjects.

    Но сдаётся мне, что моя идея с доп. мьютексами попахивает каким то мазохизмом и есть более простой способ решения проблемы)
  • rrrrrrr © (17.08.17 16:58) [1]
    зачем тебе вообще WaitForMultipleObjects?
  • rrrrrrr © (17.08.17 17:00) [2]
    и тем более мьютексы зачем тебе
  • rrrrrrr © (17.08.17 17:05) [3]
    создаем поток.
    в конструктор передаем имя файла и хендл формы откуда все это происходит.
    наращиваем счетчик запущенных.

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

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

    и все.
    никого ни ждать и пасти не надо.
  • Denchik (17.08.17 17:08) [4]
    > зачем тебе вообще WaitForMultipleObjects?

    Хочу дождаться пока потоки завершат работу, забрать из них результат работы и грохнуть их. А какие ещё варианты есть кроме OnTerminate и ручного определения что все потоки завершили работу? Вроде для этого WaitForMultipleObjects и предназначен?
  • Denchik (17.08.17 17:10) [5]
    > в конструктор передаем имя файла и хендл формы откуда все это происходит.

    Каждый поток обрабатывает не один и список файлов. Запускаю я эти потоки не из события формы, а с другого уже созданного мною потока. Кроме просчёта md5 там ещё пачка операций выполняется. Не стал загромождать топик лишним кодом.
  • Denchik (17.08.17 17:13) [6]
    Если совсем коротко, есть основной поток (потомок TThread, тоже созданный мной), который должен запустить ещё несколько потоков, дождаться их выполнения, забрать из этих потоков данные и продолжить выполнение других задач.
  • rrrrrrr © (17.08.17 17:17) [7]
    Каждый поток обрабатывает не один и список файлов.

    Ну и смысл. делай чтобы основной обрабатывал не один а несколько файлов.
    Те же будут только в профиль.
  • rrrrrrr © (17.08.17 17:18) [8]
    дождаться их выполнения,

    Зачем?

    забрать из этих потоков данные
    Зачем. Эти потоки могут сами отдать данные.
  • rrrrrrr © (17.08.17 17:21) [9]
    вот файл в сто байт.
    а рядом в терабайт.

    хеш первого будет готов не успеешь кнопку отпустить
    а хеш второго чуть опосля.

    смысл ждать первому второго если первый уже все сделал?
  • rrrrrrr © (17.08.17 17:34) [10]
    Идея, раскидывать список файлов на потоки, чтобы каждый обсчитывал свой кусок списка. Количество потоков = количеству ядер CPU в системе.

    идея конечно супер.
    жалко только, что в ней нет ничего про пропускную способность дисковой подсистемы.
  • Denchik (17.08.17 18:04) [11]
    > Ну и смысл. делай чтобы основной обрабатывал не один а несколько файлов.

    С этого я начинал. Скорость оставляет желать лучшего.

    > Те же будут только в профиль.

    Это не так, проверено. Delphi не настолько умён чтобы уметь параллелить свои задачи без ведома программера. Один мой поток на моём 4х ядерном CPU занимает максимум 25% ресурсов, т.е. максимум одно ядро. 4мя же потоками можно забить его на все 100%. При этом приоритет потока (хоть tpTimeCritical ему поставь) никак не влияет на распределние нагрузки между ядрами. Опять же через свои же шишки пришёл к тому, что приоритет имеет вес если например запустить 2 потока на одноядерном CPU. Тогда поток с большим приоритетом получит больше процессорного времени.

    > Эти потоки могут сами отдать данные.

    А каким образом не пойму? То что выдумал я, сделал класс, наследник TList, который заполняю списком фалов перед Thread.Start. Владелец этого класса сам поток. После завершения вытягиваю заветные просчитанные md5 из свойства потока. В процессе работы поток работает со своими изолированными данными которые находятся внутри самого потока. Выбрал этот путь опять же чтобы облегчить себе жизнь отсутствием всяких синхронизаций, критических секций и прочих трудностей которые возникают при совместном доступе разных потоков к общим данным.

    > смысл ждать первому второго если первый уже все сделал?

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

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

    Об этом тоже подумал. Тестил я на максимальном количестве ядер 8, обычном простеньком SATA (тупой бэкапный винт) и проблем с диском не было. Всё упирается именно в ресурсы CPU. Т.е. даже 8 ядер i7 не успевают с такой скоростью читать данные с диска и обсчитывать их, чтобы диск захлебнулся.
  • Denchik (17.08.17 18:06) [12]
    Незаметно ушли от основного вопроса..
  • rrrrrrr © (17.08.17 18:07) [13]
    вот допустим в списке 100 файлов.
    запустить сто IO операций чтения (и пусть дерутся между собой за шину sata) - идея бестолковая.

    значит одновременно должно работать скажем 10 - 20 потоков.
    идем дальше.

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

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

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

    Всё упирается именно в ресурсы CPU.

    Все упирается как раз в диск.
    Так как мд5 считается быстрее, чем файл читается с диска.
  • rrrrrrr © (17.08.17 18:10) [14]
    Задача поставлена так, что пока не будут обсчитаны все файлы, я не могу двигаться дальше.

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

    но при этом ты не будешь висеть в зачем-то вызванном WaitForMultipleObjects, а будешь волен делать все что хочешь.
  • rrrrrrr © (17.08.17 18:22) [15]
    > Эти потоки могут сами отдать данные.

    А каким образом не пойму?


    Элементарным

    SendMessage (or PostMessage) на хендл окна из которого все создавалось.
    /* первый синхронный второй асинхронный */

    для передачи результата : WParam + LParam
    которые могут быть указателями на какие угодно структуры с расчитанными потоком данные.
    Или они могут быть простым пэчаром на строку хеша приведенным к L/W Param
    Да чем угодно они могут быть
  • Denchik (17.08.17 19:00) [16]
    > значит одновременно должно работать скажем 10 - 20 потоков.

    Я не говорил что их будет 10-20. Такое количество без всяких сомнений упрётся в возможности диска.

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

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

    > зачем нам мьютексы, даже если мы знаем такое слово?

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

    > Так как мд5 считается быстрее, чем файл читается с диска.

    Это проверял собственноручно. i7-4790k, простой SATA диск, запуск в 8 потоков, тестовая куча файлов просчиталась за 9 секунд. Тоже самое в один поток - 68 секунд. Далее, ноут i5-2410m, родной SATA, один поток - 84 сек, 2 потока 47 сек. Далее были ещё более слабые Core2Duo, Celeron. Результатов нет под рукой, но ситуация не сильно отличалась.

    > SendMessage (or PostMessage) на хендл окна из которого все создавалось.

    Вопрос зачем это всё? Я могу просто забрать из потока обычным доступом к его свойству.
  • rrrrrr © (17.08.17 19:32) [17]
    В другом случае есть вероятность, что один и тот же поток полезет к одному элемента массива.</>

    вероятность есть, но равняется ровно нулю.
    если делать как тебе говорят.

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

    Вопрос зачем это всё? Я могу просто забрать из потока обычным доступом к его свойству.

    забрать-то ты можешь.
    только ты не знаешь когда это уже можно делать.

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

    удачи
  • rrrrrr © (17.08.17 19:36) [18]
    Счётчик - костыль, мьютекс (а точнее семафор, если речь идёт именно о параллельных вычислениях) - это есть тот самый счётчик только потокоориентированный.

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

    к тому же зачем синхронизировать то, что в синхронизации не нуждается.
    а расчет хэша одного файла как раз и не нуждается в синхронизации с расчетами других файлов.
  • rrrrrr © (17.08.17 19:48) [19]
    Вопрос зачем это всё? Я могу просто забрать из потока обычным доступом к его свойству.

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

    а то, что более ресурсоемко - оно еще и по затратам процессорного времени не даром тебе дается.

    ищу изящное решение вопроса.

    ищи ищи.
 
Конференция "Основная" » Снова многопоточность
Есть новые Нет новых   [118640   +43][b:0][p:0.002]