С++ 11 Поведение ожидания потока: std::this_thread::yield() против std::this_thread::sleep_for( std::chrono::milliseconds(1))

При написании кода C++ для Microsoft мне сказали, что запись Sleep(1) намного лучше, чем Sleep(0) для спин-блокировки, из-за того, что Sleep(0) будет использовать больше процессорного времени, более того, он уступает только при наличии другого равного-приоритета поток, ожидающий запуска.

Однако с библиотекой потоков С++ 11 не так много документации (по крайней мере, которую мне удалось найти) о влиянии std::this_thread::yield() на std::this_thread::sleep_for( std::chrono::milliseconds(1) ); второй, безусловно, более многословен, но одинаково ли они эффективны для спин-блокировки, или он потенциально страдает от тех же ошибок, что и Sleep(0) против 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!
}

person Thomas Russell    schedule 26.06.2013    source источник
comment
возможный дубликат std::this_thread::yield() vs std::this_thread::sleep_for ()?   -  person Stephan Dollberg    schedule 26.06.2013
comment
@bamboon Я не верю, что это дубликат, потому что он охватывает конкретный случай, ответы на этот вопрос прямо или косвенно не отвечают на мой вопрос здесь.   -  person Thomas Russell    schedule 26.06.2013
comment
Вот почему перед ним стоит возможность. ;) То, что вы спрашиваете, полностью зависит от реализации и не связано со стандартным С++, стандартная часть должна быть освещена в связанных вопросах. yield и sleep_for(1ms) - это совершенно разные семантики, в первом случае вы сообщаете реализации yield, которая может возвращаться и возвращаться через случайное время, а во втором случае говорится, что вы не хотите спать короче 1 мс.   -  person Stephan Dollberg    schedule 26.06.2013
comment
Я думаю, что термин spinlock в вашем примере вводит в заблуждение. Насколько я понимаю, spinlock используется, чтобы избежать переключения контекста, поэтому он не использует ни sleep, ни yield. Если вы не хотите тратить циклы ЦП и можете жить с возможным переключением контекста, просто используйте std::mutex или std::condition_variable, потому что, скорее всего, эти операции уже оптимизированы для вашей платформы.   -  person nosid    schedule 26.06.2013
comment
Они оба одинаково неэффективны.   -  person    schedule 26.06.2013
comment
Это не только неэффективно, но и недопустимо, поскольку bSomeCondition не является атомарным типом.   -  person zch    schedule 27.06.2013
comment
Они не просто неэффективны для использования в спин-блокировке, они безумны для использования в спин-блокировке.   -  person David Schwartz    schedule 14.02.2016


Ответы (4)


Стандарт здесь несколько нечеток, так как конкретная реализация будет в значительной степени зависеть от возможностей планирования базовой операционной системы.

При этом вы можете смело предположить несколько вещей в любой современной ОС:

  • yield откажется от текущего кванта времени и повторно вставит поток в очередь планирования. Количество времени, которое истекает до повторного выполнения потока, обычно полностью зависит от планировщика. Обратите внимание, что в Стандарте доходность рассматривается как возможность изменить график. Таким образом, реализация может полностью вернуться из yield немедленно, если пожелает. Выход никогда не помечает поток как неактивный, поэтому поток, работающий на доходе, всегда будет производить 100% нагрузку на одно ядро. Если никакие другие потоки не готовы, вы, вероятно, потеряете не более остатка текущего кванта времени, прежде чем вы снова будете запланированы.
  • sleep_* заблокирует поток как минимум на запрошенное время. Реализация может превратить sleep_for(0) в yield. С другой стороны, sleep_for(1) приостановит вашу нить. Вместо того, чтобы вернуться в очередь планирования, поток сначала переходит в другую очередь спящих потоков. Только по истечении запрошенного времени планировщик рассмотрит возможность повторной вставки потока в очередь планирования. Нагрузка, производимая небольшим сном, все равно будет очень высокой. Если запрошенное время ожидания меньше, чем системный квант времени, вы можете ожидать, что поток пропустит только один квант времени (то есть один выход, чтобы освободить активный квант времени, а затем пропустить другой), что все равно приведет к загрузке ЦП. близко или даже равно 100% на одном ядре.

Несколько слов о том, что лучше для спин-блокировки. Спин-блокировка — это предпочтительный инструмент, когда ожидается, что конкуренция за блокировку будет минимальной или вообще отсутствовать. Если в подавляющем большинстве случаев вы ожидаете, что блокировка будет доступна, спин-блокировки — дешевое и ценное решение. Однако, как только возникнут разногласия, спин-блокировки будут стоить вам денег. Если вы беспокоитесь о том, что лучше — yield или sleep, спин-блокировки — неподходящий инструмент для этой работы. Вместо этого вы должны использовать мьютекс.

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

person ComicSansMS    schedule 26.06.2013
comment
дело не в конкуренции, а в том, что такты процессора тратятся в критической секции. для неоспариваемого случая CriticalSection (Win32) Futex (Linux) занимает почти одинаковое время процессора, что похоже на двойные операции CAS. В любом случае SpinLocks можно реализовать с помощью одного CAS. - person TakeMeAsAGuest; 15.02.2019

Я только что провел тест с Visual Studio 2013 на Windows 7, Intel i7 2,8 ГГц, оптимизация режима выпуска по умолчанию.

sleep_for(nonzero) засыпает минимум на одну миллисекунду и не требует ресурсов ЦП в цикле, например:

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() (тест здесь не показан).

Для сравнения, блокировка atomic_flag для спин-блокировки занимает около 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);

Основываясь на этом, я, вероятно, без колебаний поместил бы yield в спин-блокировку, но почти наверняка не стал бы использовать sleep_for. Если вы считаете, что ваши блокировки будут много вращаться, и беспокоитесь о потреблении ресурсов процессора, я бы переключился на std::mutex, если это целесообразно в вашем приложении. Будем надеяться, что дни действительно плохой производительности std::mutex в Windows остались позади.

person Rob L    schedule 14.02.2016
comment
Я действительно надеюсь, что этих запятых не было в коде, который вы на самом деле запускали. - person imallett; 16.02.2019

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

вот еще объяснение:

  • запуск yield в цикле гарантирует, что процессор прекратит выполнение потока, но если система попытается вернуться к потоку, она просто повторит операцию yield. Это может заставить поток использовать полную 100% загрузку ядра процессора.
  • запуск sleep() или sleep_for() также является ошибкой, это заблокирует выполнение потока, но у вас будет что-то вроде времени ожидания на процессоре. Не ошибитесь, это рабочий процессор, но с самым низким приоритетом. Хотя каким-то образом работая над простыми примерами использования (полностью загруженный процессор в sleep() вдвое хуже, чем полностью загруженный рабочий процессор), если вы хотите обеспечить ответственность приложения, вам нужно что-то вроде третьего примера:
  • комбинируя! :

    std::chrono::milliseconds duration(1);
    while (true)
       {
          if(!mutex.try_lock())
          {
               std::this_thread::yield();
               std::this_thread::sleep_for(duration);
               continue;
          }
          return;
       }
    

что-то вроде этого гарантирует, что процессор будет работать так же быстро, как эта операция будет выполнена, а также sleep_for() гарантирует, что процессор будет ждать некоторое время, прежде чем даже пытаться выполнить следующую итерацию. Это время, конечно, может быть динамически (или статически) настроено в соответствии с вашими потребностями.

ваше здоровье :)

person esavier    schedule 03.11.2014
comment
не могли бы вы подробнее объяснить mutex использование вашего третьего варианта? это необходимо? - person Felix Xu; 28.11.2019

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

Ваш код будет выглядеть так:

std::unique_lock<std::mutex> lck(mtx)
while(!bSomeCondition) {
    cv.wait(lck);
}

Or

std::unique_lock<std::mutex> lck(mtx)
cv.wait(lck, [bSomeCondition](){ return !bSomeCondition; })

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

person shangjiaxuan    schedule 29.01.2019