Комментарии 16
Про SpinWait вообще какая-то ересь написана.
Сначала написано, что при использовании Thread.SpinWait()
не происходит переключение контекста потока и не передаётся управление планировщику задач Windows. А дальше написано про цикл, который при каждой итерации возвращает поток обратно в очередь ожидания. Как это возможно без переключения контекста и использования планировщика, для меня загадка.
На самом деле:
SpinWait — это самый обычный busy wait. Логической разницы между
while (!condition()) {}
и
while (!condition()) { Thread.SpinWait(); }
нет.
Разница заключается только в том, что проверка условия может быть дорогой, поэтому, чтобы не насиловать шину атомарными операциями, имеет смысл между проверками вставлять задержку, но не настолько длинную, как при Thread.Sleep(0)
и Yield
.
Ну а чтобы проц нагружать по-минимуму, под капотом Thread.SpinWait
используется инструкция PAUSE
. Это просто небольшая задержка, при которой проц отдыхает, что при HT/SMT бывает полезно.
public void SpinOnce()
{
if (NextSpinWillYield)
{
CdsSyncEtwBCLProvider.Log.SpinWait_NextSpinWillYield();
int num = (m_count >= 10) ? (m_count - 10) : m_count;
if (num % 20 == 19)
{
Thread.Sleep(1);
}
else if (num % 5 == 4)
{
Thread.Sleep(0);
}
else
{
Thread.Yield();
}
}
else
{
Thread.SpinWait(4 << m_count);
}
m_count = ((m_count == 2147483647) ? 10 : (m_count + 1));
}
При использовании Thread.SpinWait() не происходит переключение контекста потока, текущий поток не передает управление планировщику задач Windows. Вместо этого запускается холостой цикл, который при каждой итерации возвращает поток обратно в очередь ожидания (прямо как Thread.Yield()), передавая управление потоку с таким же приоритетом, но не фоновому потоку.
Что-то странное тут написано: «который при каждой итерации возвращает поток обратно в очередь ожидания» — это про Thread.Sleep(0), а не про SpinWait, который, судя по документации, просто крутит холостой цикл на указанное число итераций, безо всяких вызовов ядра в этом цикле. (UPD: пока писал, про это уже выше написали)
Без SpinWait цикл может выполняться бесконечно, потому что если нулевой процессор запустится как фоновый поток, то первый процессор заблокирует этот фоновый поток, до тех пор пока поток на первом процессоре не будет прерван.
Это — не про Windows, точнее — не про потоки обычных пользовательских программ, которые работают с приоритетами в диапазоне динамических приоритетов (1-15). Для этого диапазона планировщик в Windows, если видит, что какой-то поток вообще не получает управление, динамически повышает его приоритет на один или несколько квантов, чтобы поток имел шанс выполниться (как раз вот для подобных случаев).
Тут, кстати, на Хабре была не так давно статья (может, помните), где автор как раз модифицировал планировщик в WinXP, чтобы избежать этого динамического повышения приоритетов — оно у него работу программы для real-time нарушало.
Спасибо, поправил. Там действительно было написано неудачно.
Тут, кстати, на Хабре была не так давно статья (может, помните), где автор как раз модифицировал планировщик в WinXP, чтобы избежать этого динамического повышения приоритетов — оно у него работу программы для real-time нарушало.
Не видел, но если вдруг найдется ссылка, будет интересно почитать. Можно даже добавить сноску со ссылкой на нее в этот пост. Думаю, многим может быть интересно.
Не видел, но если вдруг найдется ссылка, будет интересно почитать.
Планировщик Windows? Это очень просто
Там об этом мельком.
А вообще информация об этом в книге «Windows Internals» уже давно есть, из изданиия в издание кочует — не помню, была ли она в первом, которое писала Хелен Кастер, но в тех издания, которые Руссинович делал, кажется, была уже сразу.
В дополнение к статье: есть ещё один механизм межпотоковой синхронизации — WaitOnAddress, появившийся в Windows 8.1, и аналогичный ему функционал futex
в Linux. Работа с ним оставила у меня положительные впечатления. Для использования из .NET, правда, нужно использовать pinvoke.
Судя по всему, речь шла о классе SpinWait который как раз использует стратегию отката к Sleep и Yield. А дальше не совсем верный пересказ о том, что busy-wait может очень сильно замедлить общий прогресс всех потоков (thread starvation), особенно если процессор с одним ядром.
По идее на процессорах и системах, которые берегут психику своих юзеров, реордеринг не должен менять однопоточное поведение. То есть итоговая программа должна линеаризироваться и сохранять видимость натурального порядка. Но тогда что это за реордеринг, который не меняет видимое поведение?
А в многопоточной системе? Гарантирует ли запрет на реордеринг 2х записей в ячейке А и Б, а потом 2х чтений оттуда же, что мы увидим при чтении тот же порядок (не увидим запись в Б и отсутствие записи А), что был при записи? Или это какое-то другое требование. Запрет на реордеринг только записей или только чтений вообще не имеет по идее никакого проверяемого эффекта. Как я понимаю, на той же альфе мы можем увидеть запись через гонку вообще в любой момент времени, а потом увидеть честную запись, а потом опять увидеть запись через гонку. В процессоре вообще кмк понятие времени сильно размазано.
Все статья по многопоточке используют эту терминологию так, будто она всем широко известна. И есть какая-то за этим великая наука, которую все знают. А я вот сижу дурак дураком и до сих пор до конца это не понимаю. А я прилично конкаренси лет 10 уже занимаюсь минимум. Может кто посоветует по этим базам какие-то ссылки или лекции?
Вот не пойму в подобных статьях, что такое реордеринг
Иногда это Out-of-order execution, а иногда Memory ordering. Без контекста действительно не всегда ясно о чем именно речь. Вы какой абзац имеете в виду?
Может кто посоветует по этим базам какие-то ссылки или лекции?
Мне кажется, что в докладе Гольдштейшна о моделях памяти простым языком хорошо эти понятия определены.
По идее на процессорах и системах, которые берегут психику своих юзеров, реордеринг не должен менять однопоточное поведение.
Вы имеете в виду SC for DRF?
Там где про гарантии железа.
>>Мне кажется, что в докладе Гольдштейшна о моделях памяти простым языком хорошо эти понятия определены.
Легче не стало.) Там тоже неявно вводится порядок и этот неявный порядок нарушается. Как будто операции это некие сущности, на которых еще и введен полный порядок, что очевидно не так. Все те же самые вопросы встают с неменьшей силой. Ну может просто я не соображаю, а остальным эти все концепции понятны. Хоть Java Memory Model мне понятна, там никаких реордерингов нет и логика строится на выстраивании отношений частичного порядка. Мне непонятно, какие именно гарантии я получаю от того, что операциям запрещен реордеринг. Особенно когда мой код проходит цепочку компилятор-jit-scheduller-cpu и они все могут вносить свои искажения в то, как он исполняется. Вот см. пример выше, если и чтения и записи идут с запретом реордеринга, я могу иметь гарантии, что не увижу их в другом порядке? А если только записи? Только чтения? Какие гарантии я получаю? Ну итд выше по списку, о каком вообще порядке может идти речь, если у меня в общем случае не линеаризуемый код.
>>Вы имеете в виду SC for DRF?
Видимо, я далек от языков низкого уровня и процессоров типа Альфы, где видимо это не всегда так. Поэтому боюсь, что термин мне незнаком. Но судя по вики очень похоже.
Про «гарантии железа» — это про модели памяти. Грубо говоря, в контексте архитектуры процессора это спецификации, определяющая какую дичь CPU может творить в плане перестановок операций. Например, вот параграф из описания модели памяти ARM:
The ARMv8-A architecture employs a weakly ordered model of memory. This means that the order of memory accesses is not necessarily required to be the same as the program order for load and store operations.
«Слабость» модели памяти ARM отражена на схеме: ARM позволяет себе гораздо больше вольностей, чем X86, поэтому, код, прекрасно работающий на x86, может быть подвержен race conditions на ARM просто потому что ARM подходит к выполнению инструкций «более творчески» (привет, новые макбуки:).
Добавим сюда еще:
- For example, in Java, this guarantee is directly specified
- By contrast, a draft C++ specification does not directly require an SC for DRF property, but merely observes that there exists a theorem providing it...Note that the C++ draft specification admits the possibility of programs that are valid but use synchronization operations with a memory_order other than memory_order_seq_cst, in which case the result may be a program which is correct but for which no guarantee of sequentially consistency is provided. In other words, in C++, some correct programs are not sequentially consistent. This approach is thought to give C++ programmers the freedom to choose faster program execution at the cost of giving up ease of reasoning about their program...
В общем, в C# я стараюсь всего это избегать и где только можно использовать статические инициализаторы, чтобы CLR сам за меня отдувался. Не думаю, что стоит сильно переживать, что вы «недостаточно умны». Вот Эрик Липперт тоже «не достаточно умен» :))
Однако знать о существовании кроличьей норы явно не будет лишним, хотя бы для того, чтобы правильно использовать более высокоуровневые абстракции.
В общем, в C# я стараюсь всего это избегать и где только можно использовать статические инициализаторы, чтобы CLR сам за меня отдувался. Не думаю, что стоит сильно переживать, что вы «недостаточно умны». Вот Эрик Липперт тоже «не достаточно умен» :))
Я бы написал так: если вам в проекте вдруг понадобилось работать с многопоточностью на низком уровне, значит, вы что-то делаете не так. Например, писать собственные lock-free коллекции — хорошая разминка для мозгов, но вот видеть это в проде я бы не хотел.
Кстати, раз уж статья достаточно строгая и было несколько замечаний — только именованные объекты синхронизации в Windows видны другим процессам.
Многопоточность на низком уровне