Проблема в многопоточности

Вопросы программирования на Free Pascal, использования компилятора и утилит.

Модератор: Модераторы

Проблема в многопоточности

Сообщение vada » 20.01.2015 15:26:58

Добрый день, форумчане!

Столкнулся со странной проблемой. Пришло время рефакторинга готовой программы. Была задача "шоб работало". Работает. Но медленно. Есть множество мест где распараллелить программу просто само просится. Ну и раскидал итерации по нитям... Тут же получил граблями в лоб. :( Анализ результатов показал что некоторые нити просто не выполняются. Бился долго. Ничего путного не получилось.
Тогда взял пример из http://www.freepascal.org/docs-html/prog/progse44.html#x234-24700010.2

Код: Выделить всё
{$mode objfpc} 

uses 
  sysutils {$ifdef unix},cthreads{$endif} ; 

const 
  threadcount = 100; 
  stringlen = 10000; 

var 
   finished : longint; 

threadvar 
   thri : ptrint; 

function f(p : pointer) : ptrint; 

var 
  s : ansistring; 

begin 
  Writeln(’thread ’,longint(p),’ started’); 
  thri:=0; 
  while (thri<stringlen) do 
    begin 
    s:=s+’1’; 
    inc(thri); 
    end; 
  Writeln(’thread ’,longint(p),’ finished’); 
  InterLockedIncrement(finished); 
  f:=0; 
end; 

var 
   i : longint; 

begin 
   finished:=0; 
   for i:=1 to threadcount do 
     BeginThread(@f,pointer(i)); 
   while finished<threadcount do ; 
   Writeln(finished); 
end. 


Все замечательно работает, но!!! Если изменить количество создаваемых нитей в сторону увеличения, например, 500, получаю ту же проблему что и в моей программе. Т.е. некоторые нити не выполняются, а зачастую программа зацикливается - нити не завершаются, или (возможно) не изменяется количество завершенных нитей.
Код: Выделить всё
const 
  threadcount = 500;  // Увеличил число нитей
  stringlen = 100;      // Уменьшил длину строки, ибо, нефиг

Опытным путем установил, что стабильно программа работает только при количестве созданных нитей меньше 128. В документации нигде не нашел ограничений на количество создаваемых нитей. В моей замечательной программе минимум нужно 150 нитей, а то и больше 1000. Или усложнять алгоритм формирования потоков и анализ их завершения. В итоге, выигрыш ноль :(

Написал еще тестовую программку которая по растровой картинке 1000 на 1000 пробегает по пикселам. Там где поток пробегаем меняю цвет белый на красный, синий на зеленый. Там где, почему-то, поток ничего не делает цвета не меняются. См. приложенный рисунок.
Снимок.PNG

В общем, вот :( Такая фигня.

На борту:
Винда седьмая со всеми обновлениями.
Лазарус 1.2.6
Фри паскаль 2.6.4
У вас нет необходимых прав для просмотра вложений в этом сообщении.
Аватара пользователя
vada
энтузиаст
 
Сообщения: 691
Зарегистрирован: 14.02.2006 13:43:17

Re: Проблема в многопоточности

Сообщение iN0k » 20.01.2015 15:56:23

у меня была аналогичная фигня ... но лимит в 64 потока ... с чем связано так и не нарыл (надобыло быстро решить), в результате изменил логику :-(
но оглядываясь на тот случай ... и правильно что логику менял, что толку от 100500 нитей если они все равно на одном камне (пусть даже и в 8-16 потоков), если это числоМолотилки то тем более
iN0k
постоялец
 
Сообщения: 146
Зарегистрирован: 18.07.2012 14:09:50

Re: Проблема в многопоточности

Сообщение vada » 20.01.2015 16:00:58

Согласен, 100 нитей на двух ядрах особого смысла не имеют, но усложнение кода создания потоков и анализ завершения съедает весь выигрыш от параллельности вычислений. По тестам, выигрыш отрицательный получается :(
Аватара пользователя
vada
энтузиаст
 
Сообщения: 691
Зарегистрирован: 14.02.2006 13:43:17

Re: Проблема в многопоточности

Сообщение iN0k » 20.01.2015 16:19:50

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

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

может видяху подключить? она мощная, пусть числа молотит? :-)

и все таки мой совет:
1. много потоков не делать
2. изменить алгоритм (вот чего то я не верю что прям одновременно надо 150 вещей сделать)
iN0k
постоялец
 
Сообщения: 146
Зарегистрирован: 18.07.2012 14:09:50

Re: Проблема в многопоточности

Сообщение vada » 20.01.2015 16:42:13

Изменил программу из http://www.freepascal.org/docs-html/prog/progse44.html#x234-24700010.2. Сделал чтоб живых было не больше 5 нитей.
Все отлично срабатывает при, практически, любом количестве запускаемых нитей.

Код: Выделить всё
{$mode objfpc}

program TestThread;

uses sysutils;

const
  ThreadCount = 1000;
  StringLen = 100;
  MaxLivingThreads = 5;

var
  Finished: LongInt;
  LivingThreads: LongInt;

threadvar
  Thri: PtrInt;

function f(P : Pointer): PtrInt;
var
  S : ansistring;
  I: LongInt;
begin
  I := LongInt(P);
  Thri := 0;
  S := '';
  while (Thri < StringLen) do
  begin
    S := S + '1';
    Inc(Thri);
  end;
  Writeln('LivingThreads=',LivingThreads,'; thread=', I, '; finished=', Finished);
  InterLockedIncrement(Finished);
  InterLockedDecrement(LivingThreads);
  f := 0;
end;

var
  I : LongInt;

{$R *.res}

begin
  Finished := 0;
  LivingThreads := 0;

  for I := 1 to ThreadCount do
  begin
    while (LivingThreads >= MaxLivingThreads) do Sleep(5);
    InterLockedIncrement(LivingThreads);
    BeginThread(@f, Pointer(I));
  end;

  while (Finished < ThreadCount) do;

  Writeln('Finished=',Finished);
end.



Так и сделаю у себя.
Спасибо.
Аватара пользователя
vada
энтузиаст
 
Сообщения: 691
Зарегистрирован: 14.02.2006 13:43:17

Re: Проблема в многопоточности

Сообщение Дож » 20.01.2015 17:29:25

А если запуск
Код: Выделить всё
   for i:=1 to threadcount do
     BeginThread(@f,pointer(i));

поменять на
Код: Выделить всё
   for i:=1 to threadcount do
     while BeginThread(@f,pointer(i)) = 0 do;

?
Аватара пользователя
Дож
энтузиаст
 
Сообщения: 899
Зарегистрирован: 12.10.2008 16:14:47

Re: Проблема в многопоточности

Сообщение vada » 20.01.2015 18:52:51

Не взлетит. ID нити ни разу не видел нулевым.
Аватара пользователя
vada
энтузиаст
 
Сообщения: 691
Зарегистрирован: 14.02.2006 13:43:17

Re: Проблема в многопоточности

Сообщение Дож » 20.01.2015 18:59:25

Не взлетит. ID нити ни разу не видел нулевым.

1) У меня работает, я проверял.
2) В документации на BeginThread сказано, что возвращается 0, если не удалось запустить поток
http://www.freepascal.org/docs-html/rtl ... hread.html
Errors

On error, the value "0" is returned.
Аватара пользователя
Дож
энтузиаст
 
Сообщения: 899
Зарегистрирован: 12.10.2008 16:14:47

Re: Проблема в многопоточности

Сообщение vada » 20.01.2015 19:17:04

Запустить поток просто. Непросто обеспечить его нормальное выполнение. Я проверял множество раз. Поток создается с ненулевым ID, но не работает. 1000 потоков... да запросто. Вот только штук 700 отработают неправильно, или вообще не отработают. И все с ненулевым ID.
Вот это замечательно работает
Код: Выделить всё
for I := 1 to ThreadCount do
  begin
    while (LivingThreads >= MaxLivingThreads) do Sleep(5);
    InterLockedIncrement(LivingThreads);
    BeginThread(@f, Pointer(I));
  end;
Аватара пользователя
vada
энтузиаст
 
Сообщения: 691
Зарегистрирован: 14.02.2006 13:43:17

Re: Проблема в многопоточности

Сообщение iN0k » 20.01.2015 21:39:58

vada писал(а):ID нити ни разу не видел нулевым.

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

тока вместо Sleep(5) лучше Sleep(0), а еще лучше использовать рекомендуемую ThreadSwitch.
iN0k
постоялец
 
Сообщения: 146
Зарегистрирован: 18.07.2012 14:09:50

Re: Проблема в многопоточности

Сообщение pda » 21.01.2015 01:43:37

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

Давайте посмотрим на этот пример и разберём его на предмет «что тут не так»:

Код: Выделить всё
uses
   sysutils {$ifdef unix},cthreads{$endif} ;

Сейчас вместе с cthreads рекомендуется так же использовать менеджер памяти из libc (модуль cmem). Здесь у нас устаревшая документация.

Код: Выделить всё
threadvar
   thri : ptrint;

Threadvar — это не для вас. Это исключительно для тех у кого нет другого выбора и кто точно знает что он делает. Мало знать какую-то директиву, надо понимать что за ней стоит. Локальные переменные потока хранятся в специальном буфере памяти, входящим в состав контекста потока и сохраняемом с ним. Документация к библиотеке C++ Boost недвусмысленно предупреждает, что размер локального хранилища потока зависит от операционной системы, её настроек и может быть очень ограниченным.
В Windows размер TLS по умолчанию 1088 ячеек (переменных) на процесс. В Linux это количество может быть ограниченно ulimit. Другие части программы могут использовать TLS незаметно для вас.

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

Код: Выделить всё
Writeln(’thread ’,longint(p),’ started’);

Каждый раз, когда вы вызываете в потоке какую-либо функцию, вы должны задумываться — чистая ли она? Не обращается ли она к данным за её пределами, особенно, не модифицирует ли она их. Например, sin() - чистая, потому что она лишь производит вычисления. А вот random() - под вопросом. Паскаль использует свой встроенный в rtl ГСЧ, который имеет внутреннее состояние. Каждый вызов random() меняет его. Насколько мне известно, эта функция не потокобезопасна и одновременное её использование может нарушить работу ГСЧ.

Но вернёмся к Writeln. Потокобезопасна ли она? Этого с ходу нагуглить не удаётся. Как минимум она записывает данные. Они попадают в консоль. Есть ли там защита от параллельного доступа?

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

Так же, при работе со сторонними функциями, например API операционной системы, следует учитывать их ограничения. Некоторые функции могут быть вызваны только из определённых потоков, например только из главного. Или из того, в котором была инициализирована какая-нибудь подсистема. (COM в Windows.)

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

Код: Выделить всё
InterLockedIncrement(finished);

Здесь у нас попытка создать наколенную синхронизацию. Interlocked-функции выглядят волшебными, но на самом деле они работают очень нехорошим образом. Эти функции, используют специальные инструкции процессора для выполнения атомарных операций, что как правило приводит к блокировке шины процессора на время выполнения такой команды. Когда ядер было 1-2 — всё было хорошо, но теперь, когда 8 ядер можно спокойно встретить на домашнем компьютере, их полезность выходит боком. В коде, часто использующим такие функции, будет работать лишь одно ядро процессора.

Здесь она однократно используется для сигнализации завершения работы. Это допустимо, но следует помнить об опасности злоупотребления ими.

Код: Выделить всё
while finished<threadcount do ;

А вот это вызывает скорбь и горечь.

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

А второй момент — приглашение к ошибке для новичков. Так синхронизироваться опасно. Современные процессоры как правило — суперскалярные, они могут изменять порядок выполнения инструкций и чтения/записи в память, для повышения производительности. Когда finished уже установлен ещё не все другие данные могут быть записаны в память.

Ну, ладно. Здесь — не могут. InterLockedIncrement() творит свою магию, устанавливая барьер на запись, вынуждая процессор завершить все более ранние операции записи до того, как завершить функцию. Но этому нет никаких пояснений. Просто ходячее приглашение для неискушённого разработчика применить Boolean флажок без interlocked-функии. Какая разница сколько раз будет перезаписан True? И... Добро пожаловать в страну глюков.

Код: Выделить всё
SuspendThread
Suspends the execution of the thread.
ResumeThread
Resumes execution of a suspended thread.

Эти функции из статьи по ссылке — ещё одно приглашение в страну неуловимых ошибок. Дело в том, что они — отладочные. Когда-то Borland добавил их не разобравшись и понеслось... Нельзя управлять потоками через них. Дело в том, что они могут остановить поток в произвольный момент, в том числе тогда, когда код в нём сделать часть какой-то важной работы, например, изменения данных большой структуры. Пока работа не завершена в ней будет смесь старого и нового.

Резюме:

Не используйте низкоуровневые функции. Используйте класс TThread, где чёрная работа уже сделана. Используйте стандартные механизмы синхронизации, вроде критических секций (TCriticalSection) или событий (Tevent). Разделите всю работу на крупные куски по количеству физических ядер и создайте ровно столько потоков. Создание, удаление, переключение потоков — дорогие операции. 1000 одновременно работающих потоков будут тормозить сильнее, чем один.

Старайтесь разносить (хотя бы на несколько сотен байт) области откуда читают и откуда пишут потоки. Синхронизация одновременной записи в одну и ту же область памяти дорогого стоит процессорам.
Аватара пользователя
pda
постоялец
 
Сообщения: 303
Зарегистрирован: 27.05.2005 19:59:53

Re: Проблема в многопоточности

Сообщение vada » 21.01.2015 10:47:01

pda
Большое спасибо за развернутый ответ. Многое стало понятно. Спасибо!
Аватара пользователя
vada
энтузиаст
 
Сообщения: 691
Зарегистрирован: 14.02.2006 13:43:17

Re: Проблема в многопоточности

Сообщение Stertor » 15.02.2015 20:49:47

Спасибо, прямо целый ман по потокам. Узнал много нового.
Аватара пользователя
Stertor
новенький
 
Сообщения: 20
Зарегистрирован: 10.08.2014 18:11:12

Re: Проблема в многопоточности

Сообщение qivi » 16.02.2015 11:30:22

pda писал(а):Разделите всю работу на крупные куски по количеству физических ядер и создайте ровно столько потоков.

А как можно во время выполнения узнать количество физических ядер?
Аватара пользователя
qivi
энтузиаст
 
Сообщения: 703
Зарегистрирован: 19.01.2009 13:45:54
Откуда: Россия

Re: Проблема в многопоточности

Сообщение Лекс Айрин » 16.02.2015 12:32:15

qivi, как минимум, есть специальная команда ассемблера, которая позволяет узнать версию процессора.
Аватара пользователя
Лекс Айрин
долгожитель
 
Сообщения: 5723
Зарегистрирован: 19.02.2013 16:54:51
Откуда: Волгоград

След.

Вернуться в Free Pascal Compiler

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 23

Рейтинг@Mail.ru