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

Правильно ли это использование Thread.MemoryBarrier()?

Предположим, у меня есть поле, которое управляет выполнением некоторого цикла:

private static bool shouldRun = true;

И у меня есть поток, который имеет код вроде:

while(shouldRun) 
{
    // Do some work ....
    Thread.MemoryBarrier();
}

Теперь другой поток может установить shouldRun в false, не используя какой-либо механизм синхронизации.

Насколько я понимаю Thread.MemoryBarrier(), этот вызов внутри цикла while будет препятствовать тому, чтобы мой рабочий поток получал кешированную версию shouldRun и эффективно предотвращал бесконечный цикл.

Насколько я понимаю, что Thread.MemoryBarrier правильно? Учитывая, что у меня есть потоки, которые могут устанавливать переменную shouldRun (это нелегко изменить), это разумный способ убедиться, что мой цикл остановится один раз shouldRun установлен на false любым потоком?

4b9b3361

Ответ 1

Правильно ли это использование Thread.MemoryBarrier()?

Нет. Предположим, что один поток устанавливает флаг перед тем, как цикл даже начинает выполняться. Цикл все равно может выполняться один раз, используя кешированное значение флага. Это верно? Мне это, безусловно, кажется неправильным. Я ожидал бы, что если я установил флаг перед первым выполнением цикла, цикл будет выполняться в нулевое время, а не один раз.

Насколько я понимаю Thread.MemoryBarrier(), этот вызов внутри цикла while будет препятствовать тому, чтобы мой рабочий поток получал кешированную версию shouldRun и эффективно предотвращал бесконечный цикл. Насколько я понимаю, что Thread.MemoryBarrier правильно?

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

Если вы чертовски настроены на выполнение кода с низким уровнем блокировки, я был бы склонен сделать поле изменчивым, а не вводить явный барьер памяти. "volatile" - это особенность языка С#. Опасная и плохо понятая особенность, но особенность языка. Он четко передает читателю код, что поле, о котором идет речь, будет использоваться без блокировок на нескольких потоках.

- это разумный способ гарантировать, что мой цикл остановится, как только shouldRun будет установлен на false любым потоком?

Некоторые люди считают это разумным. Я бы не сделал этого в своем коде без очень-очень веской причины.

Обычно методы с низкой блокировкой оправдываются соображениями производительности. Существует два таких соображения:

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

Во-вторых, может быть, что незащищенная блокировка слишком медленная. Если "работа", которую вы делаете в цикле, занимает, скажем, меньше 200 наносекунд, тогда время, необходимое для проверки незащищенного замка - около 20 нс, - значительная часть времени, затраченного на выполнение работы. В этом случае я предлагаю вам сделать больше работы за цикл. Действительно ли необходимо, чтобы цикл останавливался в течение 200 нс от управляющего флага?

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

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

Ответ 2

Я считаю, что ваше понимание немного не работает в этой строке

Насколько я понимаю Thread.MemoryBarrier(), этот вызов внутри цикла while будет препятствовать тому, чтобы мой рабочий поток получал кешированную версию shouldRun и эффективно предотвращал бесконечный цикл.

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

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

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

Ответ 3

В зависимости от того, что вы пытаетесь сделать, это, вероятно, не будет делать то, что вы ожидаете. Из MSDN, MemoryBarrier:

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

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

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

Ответ 4

Это не прямой ответ на вопрос; однако, похоже, это хорошо видно на вышеуказанных должностях. Очевидно, что не ясно, как достичь желаемого результата. На самом деле нет ничего плохого в том, чтобы использовать только предназначенные объекты синхронизации потоков:

private static readonly ManualResetEvent shutdownEvent = new ManualResetEvent(false);

//Thread 1 - continue until event is signaled
while(!shutodwnEvent.WaitOne(0)) 
{
    // Do some work ....
    Thread.MemoryBarrier();
}

//Thread 2 - signal the other thread
shutdownEvent.Set();

Другой подход заключается в использовании оператора блокировки при доступе к переменной:

private static object syncRoot = new Object();
private static bool shouldRun = true;

//Thread 1
bool bContinue;
lock(syncRoot)
    bContinue = shouldRun;

while(bContinue) 
{
    // Do some work ....
    lock(syncRoot)
        bContinue = shouldRun;
}

//Thread 2
lock(syncRoot)
    shouldRun = false;

Ответ 5

В этом случае Thread.MemoryBarrier(); является менее эффективным, но не менее безопасным вариантом

private static volatile readonly bool shouldRun = true;

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