Подтвердить что ты не робот

С++ 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!
}
4b9b3361

Ответ 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 - это очень плохо, за исключением одного случая (только работает ваше приложение, и вы знаете, что он будет в основном потреблять все ваши ресурсы)

вот больше объяснений:

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

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

что-то вроде этого обеспечит, 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; })

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