К сожалению, написанная в сообществе инструкция не всегда будет хорошей и не всегда хорошая идея — вставлять к себе в программу первый встретившийся код из интернета. Он может быть устаревшим, может содержать неточности, может... много чего может.
Давайте посмотрим на этот пример и разберём его на предмет «что тут не так»:
- Код: Выделить всё
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 одновременно работающих потоков будут тормозить сильнее, чем один.
Старайтесь разносить (хотя бы на несколько сотен байт) области откуда читают и откуда пишут потоки. Синхронизация одновременной записи в одну и ту же область памяти дорогого стоит процессорам.