С++ 11 Поведение ожидания потока: std:: this_thread:: yield() vs. std:: this_thread:: sleep_for (std:: chrono:: milliseconds (1))
Мне сообщили, что при написании специального кода на С++ для Microsoft, который пишет Sleep(1)
, намного лучше, чем Sleep(0)
для спин-блокировки, из-за того, что что Sleep(0)
будет использовать большее количество процессорного времени, более того, оно будет выдаваться только в том случае, если ожидается ожидание очередного потока с равным приоритетом.
Однако, с библиотекой потоков С++ 11, документация (по крайней мере, что мне удалось найти) не так много, о возможностях std::this_thread::yield()
vs. std::this_thread::sleep_for( std::chrono::milliseconds(1) )
; второй, безусловно, более многословный, но оба они одинаково эффективны для спин-блокировки или же они страдают от потенциально тех же самых ошибок, которые затронули Sleep(0)
vs. Sleep(1)
?
Пример цикла, в котором допустимы либо std::this_thread::yield()
, либо std::this_thread::sleep_for( std::chrono::milliseconds(1) )
:
void SpinLock( const bool& bSomeCondition )
{
// Wait for some condition to be satisfied
while( !bSomeCondition )
{
/*Either std::this_thread::yield() or
std::this_thread::sleep_for( std::chrono::milliseconds(1) )
is acceptable here.*/
}
// Do something!
}
Ответы
Ответ 1
Стандарт здесь несколько нечеткий, так как конкретная реализация во многом будет зависеть от возможностей планирования базовой операционной системы.
Сказав это, вы можете смело предположить некоторые вещи в любой современной ОС:
-
yield
откажется от текущего тайм-листа и снова вставит поток в очередь планирования. Время, которое истекает до тех пор, пока поток не будет выполнен повторно, обычно полностью зависит от планировщика. Обратите внимание, что стандарт говорит о урожайности как о возможности перепланирования. Таким образом, реализация полностью бесплатна немедленно возвращаться с урожая, если она того пожелает. Выход никогда не будет отмечать нить как неактивную, поэтому нить, вращающаяся с выходом, всегда будет производить 100% нагрузку на одно ядро. Если никакие другие потоки не готовы, вы, скорее всего, потеряете максимум оставшуюся часть текущего тайм-листа, прежде чем снова получите расписание.
-
sleep_*
будет блокировать поток, по крайней мере, для требуемого количества времени. Реализация может превратить sleep_for(0)
в yield
. С другой стороны, sleep_for(1)
отправит вашу нить в подвеску. Вместо того, чтобы возвращаться к очереди планирования, поток сначала переходит в другую очередь спящих потоков. Только после того, как запрошенный промежуток времени пройдет, планировщик рассмотрит вопрос о повторной вставке потока в очередь планирования. Нагрузка, создаваемая небольшим сном, по-прежнему будет очень высокой. Если запрошенное время сна меньше, чем системный тайм-лист, вы можете ожидать, что поток будет пропускать только один тайм-лист (то есть один выход для освобождения активного тайм-листа, а затем пропустить его после), что по-прежнему приведет к загрузке процессора близким или даже равным 100% на одном ядре.
Несколько слов о том, что лучше для спин-блокировки. Спин-блокировка является инструментом выбора, когда вы ожидаете, что на замке не будет никаких разногласий. Если в подавляющем большинстве случаев вы ожидаете, что замок будет доступен, спин-замки станут дешевым и ценным решением. Однако, как только у вас возникнут сомнения, спин-замки будут стоить вам. Если вы беспокоитесь о том, является ли выход или сон лучшим решением, здесь спин-блокировки являются неправильным инструментом для работы. Вместо этого вы должны использовать мьютексы.
Для спин-блокировки случай, когда вы действительно должны ждать блокировки, должен считаться исключительным. Поэтому совершенно нормально просто здесь выступить - он четко выражает намерение, и тратить время процессора никогда не должно быть проблемой в первую очередь.
Ответ 2
Я только что прошел тест с Visual Studio 2013 на Windows 7, 2.8 ГГц Intel i7, оптимизация режима выпуска по умолчанию.
sleep_for (отличное от нуля) появляется спящий режим для минимума около одной миллисекунды и не требует ресурсов ЦП в цикле, например:
for (int k = 0; k < 1000; ++k)
std::this_thread::sleep_for(std::chrono::nanoseconds(1));
Эта петля 1000 снов занимает около 1 секунды, если вы используете 1 наносекунду, 1 микросекунду или 1 миллисекунду. С другой стороны, yield() занимает около 0,25 микросекунды каждый, но будет вращать процессор до 100% для потока:
for (int k = 0; k < 4,000,000; ++k) (commas added for clarity)
std::this_thread::yield();
std:: this_thread:: sleep_for ((std:: chrono:: nanoseconds (0)), похоже, примерно совпадает с yield() (тест здесь не показан).
Для сравнения, блокировка атомного флага для спин-блокировки занимает около 5 наносекунд. Этот цикл равен 1 секунде:
std::atomic_flag f = ATOMIC_FLAG_INIT;
for (int k = 0; k < 200,000,000; ++k)
f.test_and_set();
Кроме того, мьютекс занимает около 50 наносекунд, 1 секунду для этого цикла:
for (int k = 0; k < 20,000,000; ++k)
std::lock_guard<std::mutex> lock(g_mutex);
Основываясь на этом, я, вероятно, не стесняюсь вкладывать урожай в спин-блокировку, но я почти наверняка не использовал бы sleep_for. Если вы считаете, что ваши замки будут сильно вращаться и беспокоятся о потреблении процессора, я бы переключился на std:: mutex, если это практично в вашем приложении. Надеемся, что дни действительно плохой производительности на std:: mutex в Windows позади.
Ответ 3
если вы заинтересованы в загрузке процессора при использовании yield - это очень плохо, за исключением одного случая (только работает ваше приложение, и вы знаете, что он будет в основном потреблять все ваши ресурсы)
вот больше объяснений:
что-то вроде этого обеспечит, cpu будет работать так же быстро, как эта операция будет выполнена, а также sleep_for() будет гарантировать, что процессор будет ждать некоторое время, прежде чем даже попытаться выполнить следующую итерацию. На этот раз может быть, конечно, динамически (или статично) настроено в соответствии с вашими потребностями.
cheers:)
Ответ 4
То, что вы хотите, это, вероятно, переменная условия. Условная переменная с условной функцией пробуждения, как правило, реализуется так, как вы пишете, с ожиданием или ожиданием условия в цикле.
Ваш код будет выглядеть так:
std::unique_lock<std::mutex> lck(mtx)
while(!bSomeCondition) {
cv.wait(lck);
}
Или же
std::unique_lock<std::mutex> lck(mtx)
cv.wait(lck, [bSomeCondition](){ return !bSomeCondition; })
Все, что вам нужно сделать, это уведомить переменную условия в другом потоке, когда данные будут готовы. Однако вы не можете аннулировать блокировку там, если хотите использовать условную переменную.