Из того, что я читал, архитектуры процессоров Intel обеспечивают более сильную модель памяти, чем требуется в реализациях .net. Насколько правильно для кода использовать гарантии, предоставляемые процессорами Intel, или в какой степени код должен добавлять барьеры памяти, которые не потребуются для реализации Intel, в случае, если код переносится на платформу с более слабой модель памяти? Было бы уместно определить статический класс с методами, например, «выполнить барьер памяти при использовании слабой модели памяти» и потребовать, чтобы код был связан либо с «сильной моделью», либо с версией «слабой модели» этой библиотеки, в зависимости от ситуации? В качестве альтернативы, можно было бы использовать Reflection для генерации такого статического класса при запуске программы таким образом, чтобы JIT-компилятор мог, при использовании сильной модели, "встроить-развернуть" инструкции "барьер памяти, если слабый" до нуля (т. Е. Опустить их полностью из кода JITted)?
Если бы у меня были мои druthers, .net предоставил бы вариант класса MemoryLock
с некоторыми операциями с полублокировкой, которые потребовали бы, чтобы все потоки, которые удерживают полублокировку, следовали этой модели памяти с полублокировкой. В системе с очень сильной моделью памяти полублокировки ничего не сделают. В системе с очень слабой моделью памяти любой поток, желающий войти в полублокировку, в которой уже был другой поток, должен был бы ждать, пока либо первый поток не завершит работу, либо его можно будет запланировать с помощью ЦП или ядра (на основе на модели, заданной полублоком), который использовал первый поток. Обратите внимание, что в отличие от обычной блокировки, MemoryLock
никогда не может быть заблокирован, поскольку любая комбинация конфликтующих требований блокировки может быть разрешена путем планирования всех потоков для запуска на одном и том же ЦП, и система может освободить любой MemoryLock
, удерживаемый потоком, который умирает (поскольку цель из MemoryLock
будет защищать ресурсы от доступа способами, которые нарушили бы модель памяти, и мертвый поток, конечно, не может сделать такой доступ).
Конечно, в .net 4.0 такого не существует; учитывая это, как лучше всего справиться с существующей ситуацией? Перенос кода, который разработан для более сильной модели памяти, в систему с более слабой моделью, при отсутствии некоторых средств для принудительного применения более сильной модели, был бы рецептом катастрофы, но добавление большого количества Lock
или MemoryBarrier
вызовов, которые были бы ненужными для исходная целевая платформа кода не кажется очень привлекательной. Единственный известный мне способ заставить код использовать сильную модель памяти - это установить для каждого потока привязку к процессору. Если бы существовал способ установить параметр процесса, чтобы .net использовала только одно ядро за раз, это могло бы быть полезно (особенно если это означало, что JIT может заменить заблокированные операции с блокировкой шины более быстрыми эквивалентами без блокировки шины) , но единственный известный мне способ установки сродства ЦП ограничит программу использованием определенного выбранного ЦП для всех своих потоков, даже если этот ЦП был сильно загружен другими приложениями, а какой-то другой ЦП бездействовал.
Дополнение
Рассмотрим следующий код:
// Thread 1 -- Assume that at start SharedPerson points to a Person "Smiley", "George" var newPerson = new Person(); newPerson.LastName = "Simpson"; newPerson.FirstName = "Bart"; // MaybeMemoryBarrier1 SharedPerson = newPerson; // Thread 2 var wasPerson = SharedPerson; // MaybeMemoryBarrier2 var wasLastName = wasPerson.FirstName; var WasFirstName = wasPerson.LastName;
Насколько я понимаю, даже при отсутствии барьеров памяти, код, работающий на процессоре Intel, гарантирует, что записи не будут повторно упорядочены; следовательно, в теме 2 читаемый человек будет либо «Смайлик», «Джордж», либо «Симпсон», «Барт». Однако модель памяти .net слабее, и программа .net может оказаться запущенной на процессоре, где поток 2 может увидеть неполный объект (поскольку запись в SharedPerson
могла произойти до записи в newPerson.FirstName
). Добавление барьера памяти в MaybeMemoryBarrier1
позволило бы избежать этой опасности, но барьеры памяти влияют на производительность независимо от того, нужны они на самом деле или нет.
Я не думаю, что минимально необходимая модель памяти .net настолько слабая, чтобы требовать MaybeMemoryBarrier2
в тех случаях, когда поток 2 гарантированно никогда не получит доступ к объекту, на который ссылается SharedPerson
, до чтения самого SharedPerson
(как это было бы в случае приведенный выше код, поскольку новый экземпляр не подвергается воздействию внешнего кода до того, как он будет сохранен в SharedPerson
). С другой стороны, предположим, что ситуация немного изменилась, поэтому Thread 2
создал JobInfo
запись, которую затем поместил в очередь для Thread 1
(при условии наличия всех необходимых блокировок и барьеров памяти для самой очереди); после этого процессоры делают:
// Thread 1 var newJob = JobQueue.GetJob(); // Gets JobInfo that was written by Thread2 newJob.StartTime = DateTime.Now(); // Eight-byte struct might straddle cache line // Will never be changed once written // MaybeMemoryBarrier1 CurrentJob = newJob; // Thread 2 var wasJob = CurrentJob; // MaybeMemoryBarrier2 var wasStartTime = CurrentJob.StartTime();
Если у потока 1 есть барьер памяти, а у потока 2 нет, есть ли гарантия, что когда поток 2 увидит запись JobInfo
, созданную им, появившуюся в CurrentJob
, он правильно прочитает свое поле StartTime
(и не увидит ни кешированного, ни частичного кэшированное значение, оставшееся с того времени, когда Thread 2
манипулировал этим объектом?
Interlocked.CompareExchange
, чтобы гарантировать победу одного из них, но не имеет значения, если оба хранилища выполняются отдельно. С другой стороны, если проблемы с потоками означают, что некоторые потоки могут не ... - person supercat   schedule 16.06.2012