Дросселирование семафора для уменьшения состязательности между потоками

We use cookies. Read the Privacy and Cookie Policy

Дросселирование семафора для уменьшения состязательности между потоками

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

Если ни один из этих методов не дает желаемого улучшения, то могло бы показаться, что нет иного выхода, кроме как уменьшить количество потоков, но при этом вы будете вынуждены заставлять одиночные потоки мультиплексировать те операции, которые естественнее было бы распределить между несколькими потоками. Выход из этой ситуации обеспечивают семафоры, которые дают возможность сохранить естественную многопоточную модель, но вместе с тем свести к минимуму количество активных потоков, конкурирующих между собой. Такое решение является концептуально простым, и его можно без труда включить в любую существующую прикладную программу, например TimedMutualExclusion. В системе "хозяин/рабочий" решение, носящее название "дросселя" семафора (semaphore throttle), использует следующую методику:

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

• Каждый рабочий поток ожидает перехода семафора в сигнальное состояние, прежде чем войти в критический участок кода. Ожидание семафора может непосредственно предшествовать ожиданию мьютекса или объекта CS.

• Если максимальное значение счетчика семафора равно 1, то использование мьютекса становится излишним. Подобное решение нередко является наилучшим для SMP-систем. 

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

Счетчик семафора просто представляет число потоков, которые могут быть активными в любой момент времени, и ограничивает количество потоков, соревнующихся между собой за право владения мьютексом, объектом CS или иным ресурсом. Главный поток даже может регулировать, или, как говорят, "дросселировать" (throttle) выполнение рабочих потоков и динамически настраивать работу приложения, ожидая, пока не уменьшится значение счетчика, если он решает, что уровень состязательности слишком высок, или увеличивая значение счетчика с помощью функции ReleaseSemaphore, чтобы дать возможность выполняться большему количеству потоков. Заметьте, однако, что максимальное значение счетчика семафора устанавливается при его создании, и изменить его впоследствии невозможно.

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

while (TRUE) { // Рабочий цикл

 WaitForSingleObject(hThrottleSem, INFINITE);

 WaitForSingleObject(hMutex, INFINITE);

 … Критический участок кода …

 ReleaseMutex(hMutex);

 ReleaseSemaphore(hThrottleSem, 1, NULL);

} // Конец рабочего цикла

Можно предложить еще одну разновидность программы. Если некоторый рабочий поток потребляет "слишком много" ресурсов, то можно заставить его выжидать некоторое время, пока значение счетчика семафора не уменьшится на несколько единиц. Однако, как уже отмечалось в предыдущей главе, использование двух последовательных циклов ожидания может стать причиной взаимоблокировки (deadlock) потоков. В следующей главе в одном из упражнений (упражнение 10.11) показано, как построить сложный объект семафора, допускающий атомарное выполнение многократных функций ожидания.

В уже упоминавшемся примере программы TimedMutualExclusion добавлен шестой параметр, являющийся начальным значением дроссельного счетчика семафора для количества активных потоков. Вы можете поэкспериментировать со значениями этого счетчика, как предлагается в одном из упражнений. На рис. 9.1 показана зависимость различных временных характеристик для шести потоков, синхронизируемых посредством одного объекта CS, от количества активных потоков, изменяющегося в интервале от 1 до 6. Во всех случаях объем выполняемой работы остается одним и тем же, но истекшее время резко увеличивается, когда количество активных потоков превышает 4.

Рис. 9.1. Зависимость производительности от количества потоков

Указанные зависимости получены на устаревших, медленных системах. Для системы Windows 2000 на базе процессора Intel 586 (Pentium), характеризующейся гораздо более высоким быстродействием, соответствующие значения истекшего времени для 1–6 потоков составили (в секундах) 0.8, 0.8, 2.3, 21.2, 28.4 и 29.0, и эти результаты могут быть последовательно воспроизведены. В этом случае ухудшение производительности становилось заметным, начиная уже с 3 активных потоков. В то же время, соответствующие временные характеристики, оцененные с использованием произвольно выбранной совокупности аналогичных систем, оказались примерно постоянными, независимо от количества активных потоков. Результаты некоторых экспериментов дают основания сделать следующие выводы:

• В системе NT5 достигнут значительный прогресс по сравнению с NT4, по следовательно демонстрирующей результаты, аналогичные тем, которые представлены на рис. 9.1.

• Получаемые результаты зависят от того, в каком режиме выполняются операции — приоритетном или фоновом, то есть, находится или не находится фокус на окне приложения, а также от присутствия в системе других выполняемых задач. 

• Как правило, мьютексы работают медленнее по сравнению с объектами CS, но в случае NT5 результаты остаются примерно постоянными, независимо от количества активных потоков.

• В SMP-системах наиболее предпочтительным вариантом является дросселирование семафора при значении счетчика равном 1. В этом случае мьютексы становятся ненужными. Так, в случае двухпроцессорной системы Xeon частотой 1.8 ГГц использованные времена для варианта CS при 1, 2 и 4 активных потоках составили 1.8, 33.0 и 31.9 секунды. Соответствующие времена в случае мьютекса составили 34.0, 66.5 и 65.0 секунды.

Резюме. Дросселирование семафоров может обеспечивать хорошую производительность как для приоритетных, так и для фоновых операций даже в случае систем, загруженных выполнением других-задач. Дроссели семафоров могут играть очень важную роль в случае SMP-систем, для которых количество активных потоков должно быть равным 1. В том, что касается производительности, семафоры, по-видимому, более эффективны, чем мьютексы.