У EventWaitHandle есть какой-то неявный MemoryBarrier? - PullRequest
9 голосов
/ 25 марта 2009

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

Я часто кодировал что-то по образцу, приведенному ниже (с такими вещами, как Dispose, для ясности опущено). Мой вопрос: летучие вещества нужны, как показано? Или ManualResetEvent.Set имеет неявный барьер памяти, как я прочитал Thread.Start? Или явный вызов MemoryBarrier будет лучше, чем volatiles? Или это совершенно неправильно? Кроме того, тот факт, что «неявное поведение барьера памяти» в некоторых операциях, насколько я видел, не задокументирован, весьма раздражает, есть ли где-нибудь список этих операций?

Спасибо, Том

class OneUseBackgroundOp
{

   // background args
   private string _x;
   private object _y;
   private long _z;

   // background results
   private volatile DateTime _a
   private volatile double _b;
   private volatile object _c;

   // thread control
   private Thread _task;
   private ManualResetEvent _completedSignal;
   private volatile bool _completed;

   public bool DoSomething(string x, object y, long z, int initialWaitMs)
   {
      bool doneWithinWait;

      _x = x;
      _y = y;
      _z = z;

      _completedSignal = new ManualResetEvent(false);

      _task = new Thread(new ThreadStart(Task));
      _task.IsBackground = true;
      _task.Start()

      doneWithinWait = _completedSignal.WaitOne(initialWaitMs);

      return doneWithinWait;

   }

   public bool Completed
   {
      get
      {
         return _completed;
      }
   }

   /* public getters for the result fields go here, with an exception
      thrown if _completed is not true; */

   private void Task()
   {
      // args x, y, and z are written once, before the Thread.Start
      //    implicit memory barrier so they may be accessed freely.

      // possibly long-running work goes here

      // with the work completed, assign the result fields _a, _b, _c here

      _completed = true;
      _completedSignal.Set();

   }

}

Ответы [ 5 ]

3 голосов
/ 18 мая 2012

Функции ожидания имеют неявный барьер памяти. Смотри http://msdn.microsoft.com/en-us/library/ms686355(v=vs.85).aspx

3 голосов
/ 25 марта 2009

Ключевое слово volatile не следует путать с тем, чтобы сделать _a, _b и _c поточно-ориентированными. См. здесь для лучшего объяснения. Кроме того, ManualResetEvent не имеет никакого отношения к безопасности потоков _a, _b и _c. Вы должны управлять этим отдельно.

РЕДАКТИРОВАТЬ: с помощью этого редактирования я пытаюсь отфильтровать всю информацию, которая была включена в различные ответы и комментарии по этому вопросу.

Основной вопрос заключается в том, будут ли переменные результата (_a, _b и _c) «видимыми» в тот момент, когда переменная флага (_completed) вернет true.

Давайте на минутку предположим, что ни одна из переменных не помечена как изменчивая. В этом случае было бы возможно установить переменные результата после переменная флага установлена ​​в Task (), например:

   private void Task()
   {
      // possibly long-running work goes here
      _completed = true;
      _a = result1;
      _b = result2;
      _c = result3;
      _completedSignal.Set();
   }

Это явно не то, что мы хотим, так как нам с этим бороться?

Если эти переменные помечены как изменчивые, то это изменение порядка будет предотвращено. Но именно это побудило исходный вопрос - требуются ли летучие вещества или ManualResetEvent обеспечивает неявный барьер памяти, так что переупорядочение не происходит, и в этом случае ключевое слово volatile не является действительно необходимым?

Если я правильно понимаю, позиция wekempf заключается в том, что функция WaitOne () обеспечивает неявный барьер памяти, который решает проблему. НО , что мне кажется недостаточным. Основной и фоновый потоки могут выполняться на двух отдельных процессорах. Таким образом, если Set () также не обеспечивает неявный барьер памяти, то в конечном итоге функция Task () может быть выполнена на одном из процессоров (даже с переменными переменными):

   private void Task()
   {
      // possibly long-running work goes here
      _completedSignal.Set();
      _a = result1;
      _b = result2;
      _c = result3;
      _completed = true;
   }

Я искал все выше и ниже информацию о барьерах памяти и EventWaitHandles, и ничего не нашел. Единственное упоминание, которое я видел, это то, что wekempf сделал для книги Джеффри Рихтера. Проблема с этим заключается в том, что EventWaitHandle предназначен для синхронизации потоков, а не доступа к данным. Я никогда не видел ни одного примера, где EventWaitHandle (например, ManualResetEvent) используется для синхронизации доступа к данным. Поэтому мне трудно поверить, что EventWaitHandle делает что-либо в отношении барьеров памяти. В противном случае я бы ожидал найти некоторую ссылку на это в Интернете.

РЕДАКТИРОВАТЬ # 2: Это ответ на ответ wekempf на мой ответ ...;)

Мне удалось прочитать раздел из книги Джеффри Рихтера на amazon.com. Со страницы 628 (wekempf цитирует это тоже):

Наконец, я должен отметить, что всякий раз, когда поток вызывает блокированный метод, процессор вызывает когерентность кэша. Поэтому, если вы манипулируете переменными с помощью взаимосвязанных методов, вам не нужно беспокоиться обо всех этих вещах модели памяти. Кроме того, все блокировки синхронизации потоков ( Monitor , ReaderWriterLock , Mutex , Semaphone , AutoResetEvent , ManualResetEvent и т. Д.) Внутренне вызывает взаимосвязанные методы.

Таким образом, может показаться, что, как указал wekempf, переменные результата не требуют ключевое слово volatile в примере, как показано, поскольку ManualResetEvent обеспечивает когерентность кэша.

Прежде чем закрыть это редактирование, я хотел бы сделать еще два замечания.

Во-первых, я исходил из того, что фоновый поток потенциально может запускаться несколько раз. Я очевидно пропустил название класса (OneUseBackgroundOp)! Учитывая, что он запускается только один раз, мне не ясно, почему функция DoSomething () вызывает WaitOne () так, как она это делает. Какой смысл ждать initialWaitMs миллисекунд, если фоновый поток может выполняться или не выполняться во время возврата DoSomething ()? Почему бы просто не запустить фоновый поток и использовать блокировку для синхронизации доступа к переменным результатов ИЛИ , просто выполнить содержимое функции Task () как часть потока, который вызывает DoSomething ( )? Есть ли причина не делать этого?

Во-вторых, мне кажется, что не использовать какой-то механизм блокировки переменных результатов по-прежнему плохой подход. Правда, это не нужно в коде, как показано. Но в какой-то момент в будущем может возникнуть другой поток и попытаться получить доступ к данным. Мне бы лучше подготовиться к этой возможности сейчас, чем пытаться выследить загадочные аномалии поведения позже.

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

3 голосов
/ 25 марта 2009

Обратите внимание, что это не так, без тщательного изучения вашего кода. Я не думаю Set выполняет барьер памяти, но я не понимаю, насколько это актуально в вашем коде? Похоже, что более важным было бы, если Wait выполняет один, что он делает. Поэтому, если я не пропустил что-то за 10 секунд, которые я посвятил просмотру вашего кода, я не верю, что вам нужны летучие вещества.

Редактировать: комментарии слишком строгие. Я сейчас имею в виду редактирование Мэтта.

Мэтт хорошо справился со своей оценкой, но ему не хватает детали. Во-первых, давайте предоставим некоторые определения вещей, разбросанных, но не разъясненных здесь.

Энергозависимое чтение считывает значение, а затем делает недействительным кэш-память ЦП. Энергозависимая запись очищает кэш, а затем записывает значение. Барьер памяти очищает кэш, а затем делает его недействительным.

Модель памяти .NET гарантирует, что все записи изменчивы. Чтения по умолчанию не выполняются, если не указано явное VolatileRead или в поле указано ключевое слово volatile. Кроме того, взаимосвязанные методы вызывают согласованность кэша, и все концепции синхронизации (Monitor, ReaderWriterLock, Mutex, Semaphore, AutoResetEvent, ManualResetEvent и т. Д.) Вызывают внутренние блокированные методы и, таким образом, обеспечивают согласованность кэша.

Опять же, все это из книги Джеффри Рихтера "CLR via C #".

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

Я придерживаюсь своего первоначального утверждения, что энергозависимость здесь не нужна.

Редактировать 2: Выглядит так, как будто вы строите «будущее». Я бы посоветовал вам взглянуть на PFX , а не на свои собственные.

1 голос
/ 25 марта 2009

Во-первых, я не уверен, должен ли я "ответить на свой вопрос" или использовать комментарий для этого, но здесь идет речь:

Мое понимание состоит в том, что volatile предотвращает перемещение кода / оптимизации памяти к доступным переменным моего результата (и к завершенным логическим значениям) так, что поток, который читает результат, будет видеть обновленные данные.

Вы бы не хотели, чтобы _completed boolean делался видимым для всех потоков после Set () из-за оптимизации / переупорядочения компилятора или emmpry. Аналогично, вы не хотели бы, чтобы записи результатов _a, _b, _c были видны после Set ().

РЕДАКТИРОВАТЬ: Дальнейшее объяснение / разъяснение по этому вопросу, в отношении пунктов, упомянутых Мэттом Дэвисом:

Наконец, я должен отметить, что всякий раз, когда поток вызывает блокировку метод, процессор заставляет кеш когерентность. Так что если вы манипулируете переменные с помощью взаимосвязанных методов, вы не нужно беспокоиться обо всем этом модель памяти вещи. Кроме того, все блокировки синхронизации потоков (монитор, ReaderWriterLock, Mutex, Semaphone, AutoResetEvent, ManualResetEvent, и т. д.) вызывать заблокированные методы внутри.

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

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

Но мешает ли это перезаписи, чтобы гарантировать, что ОБА результаты будут присвоены до флага завершения, и что завершенному флагу будет присвоено значение true до Установлен ManualResetEvent?

Во-первых, я исходил из того, что фоновый поток будет потенциально запускаться несколько раз. я очевидно упустил из виду название класс (OneUseBackgroundOp)! При условии он запускается только один раз, не ясно мне почему функция DoSomething () вызывает WaitOne () таким образом, чтобы делает. Какой смысл ждать initialWaitMs миллисекунды, если фоновый поток может или не может быть сделано в то время, когда DoSomething () возвращается? Почему бы просто не начать фоновый поток и использовать блокировку синхронизировать доступ к результатам переменные ИЛИ просто выполнить содержимое функции Task () как часть потока, которая вызывает Сделай что-нибудь()? Есть ли причина не сделать это?

Концепция примера заключается в выполнении задачи , возможно, долгосрочного выполнения. Если задача может быть выполнена в течение определенного времени, то вызывающий поток получит доступ к результату и продолжит обычную обработку. Но иногда задача может занять довольно много времени, и поток заявок не может быть заблокирован на этот период и может предпринять разумные шаги для решения этой проблемы. Это может включать проверку позже операции с использованием свойства Completed.

Конкретный пример: разрешение DNS часто выполняется очень быстро (менее секунды) и его стоит ждать даже с помощью графического интерфейса, но иногда это может занять много-много секунд. Таким образом, используя служебный класс, подобный примеру, можно легко получить результат с точки зрения вызывающего в 95% случаев и не блокировать графический интерфейс остальных 5%. Можно использовать фонового работника, но это может быть излишним для операции, которая в большинстве случаев не требует всей этой сантехники.

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

Данные результата (и флаг завершения) предназначены для однократной записи и многократного чтения. Если бы я добавил блокировку, чтобы назначить результаты и флаг, мне также пришлось бы заблокировать мои получатели результатов, и мне никогда не нравилось видеть блокировку получателей только для того, чтобы вернуть точку данных. Из моего прочтения такая мелкозернистая блокировка не подходит. Если операция имеет 5 или 6 результатов, вызывающая сторона должна взять и отпустить блокировку 5 или 6 раз без необходимости.

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

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

0 голосов
/ 25 марта 2009

Исходя из того, что вы показали, я бы сказал, что нет, volatiles не требуется в этом коде.

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...