Это первая часть из серии статей о модели памяти в языке C#; все кратко и по делу. В частности, там дается объяснение, почему в правильной реализации синглтона с двойной проверкой (double-checked lock Singleton) нужно, чтобы статическое поле было с ключевым словом volatile.
Здравствуйте, SergeyT., Вы писали:
ST>В последнем выпуске MSDN Magazine вышла отличная статья Игоря Островского"The C# Memory Model in Theory and Practice".
ST>Это первая часть из серии статей о модели памяти в языке C#; все кратко и по делу. В частности, там дается объяснение, почему в правильной реализации синглтона с двойной проверкой (double-checked lock Singleton) нужно, чтобы статическое поле было с ключевым словом volatile.
А на русском статьи нет? Английский, конечно, хорошо, но пятница... утро... что-то тяжко.
Здравствуйте, Nikolay_Ch, Вы писали:
ST>>Это первая часть из серии статей о модели памяти в языке C#; все кратко и по делу. В частности, там дается объяснение, почему в правильной реализации синглтона с двойной проверкой (double-checked lock Singleton) нужно, чтобы статическое поле было с ключевым словом volatile. N_C>А на русском статьи нет? Английский, конечно, хорошо, но пятница... утро... что-то тяжко.
На русском обычно стать выходят с некоторой задержкой. Кажется, где-то в месяц.
Здравствуйте, SergeyT., Вы писали:
ST>На русском обычно стать выходят с некоторой задержкой. Кажется, где-то в месяц.
О... Через месяц будет еще актуальнее... На праздниках, боюсь, точно аглицкий не прокатит.
Здравствуйте, SergeyT., Вы писали:
ST>В последнем выпуске MSDN Magazine вышла отличная статья Игоря Островского"The C# Memory Model in Theory and Practice".
ST>Это первая часть из серии статей о модели памяти в языке C#; все кратко и по делу. В частности, там дается объяснение, почему в правильной реализации синглтона с двойной проверкой (double-checked lock Singleton) нужно, чтобы статическое поле было с ключевым словом volatile.
Прочитал. Хотелось бы обсудить.
Рихтер в CLR via C# (основываюсь на втором издании) писал, что в CLR принята модель памяти Microsoft, которая не полностью соответствует модели ECMA. То есть в модели памяти Microsoft двойной проверки с блокировкой вполне достаточно и без указания volatile.
Так как быть, писать более строгий код, соответствующий ECMA, или ограничиться менее строгим, если приложение рассчитано только на Microsoft CLR?
Жду второй части статьи, там обещано рассказать именно о различиях с учётом разных архитектур.
class BoxedInt2
{
public readonly int _value = 42;
void PrintValue()
{
Console.WriteLine(_value);
}
}
Now, it’s possible — at least in theory — that PrintValue will print “0” due to a memory-model issue.
Here’s a usage example of BoxedInt that allows it:
class Tester
{
BoxedInt2 _box = null;
public void Set() {
_box = new BoxedInt2();
}
public void Print() {
var b = _box;
if (b != null) b.PrintValue();
}
}
Because the BoxedInt instance was incorrectly published (through a non-volatile field, _box), the thread that calls Print may observe a partially constructed object!
Я никак не соображу, как это возможно на практике (рассматриваем только реализацию рантайма от МС))? Для x86/x64 с их strong memory model volatile по сути отключает оптимизации компилятора:
The mainstream x86 and x64 processors implement a strong memory model where memory access is effectively volatile. So, a volatile field forces the compiler to avoid some high-level optimizations like hoisting a read out of a loop, but otherwise results in the same assembly code as a non-volatile read.
На итаниумах (и, подозреваю на армах) — запись всё равно volatile даже у обычных полей (оттуда жа):
A non-volatile write could just update the value in the thread’s cache, and not the value in main memory
...
However, in C# all writes are volatile (unlike say in Java), regardless of whether you write to a volatile or a non-volatile field.
т.е вот этот сценарий невозможен:
public void Set() {
// в переменную _box записывается ссылка на созданный объект, но _box._value оказывается в другой линейке кэша и ещё не скинута в оперативку.
_box = new BoxedInt2();
// Поток 2 вызывает _box.Print()
// в этот момент в память скидывается другая линейка кэша, в оперативке по адресу *_box._value оказывается 42
}
Собственно, как???
Набросал пример
class BoxedInt2
{
public readonly int _value = 42;
public void PrintValue()
{
PrintValue2(_value);
}
private void PrintValue2(int value)
{
if (value == 0)
{
throw new Exception();
}
//Console.WriteLine(value);
}
}
class Tester
{
BoxedInt2 _box = null;
public void Set()
{
_box = new BoxedInt2();
}
public void Print()
{
var b = _box;
if (b != null) b.PrintValue();
}
}
private static void Main()
{
var tester = new Tester();
var setThread = new Thread(() =>
{
while (true)
{
tester.Set();
}
});
setThread.Start();
while (true)
{
tester.Print();
}
}
мне тоже интересно
S>Because the BoxedInt instance was incorrectly published (through a non-volatile field, _box), the thread that calls Print may observe a partially constructed object! S>[/q]
это же ссылки. создание объекта и присвоение ссылки на него совершенно разные операции. что это за лажа?
Здравствуйте, Константин Л., Вы писали:
КЛ>это же ссылки. создание объекта и присвоение ссылки на него совершенно разные операции. что это за лажа?
У меня только одна версия — в статье речь не о рантайме МС, а об абстрактной модели памяти по ECMA. Вот тут у Игоря приведён похожий пример, но там явно оговаривается что речь — о модели с nonvolatile write:
You may find it surprising that a volatile read refreshes the entire cache, not just the read value. Similarly, a volatile write (i.e., every C# write) flushes the entire cache, not just the written value. These semantics are sometimes referred to as “strong volatile semantics”.
The original Java memory model designed in 1995 was based on weak volatile semantics, but was changed in 2004 to strong volatile. The weak volatile model is very inconvenient. One example of the problem is that the “safe publication” pattern is not safe. Consider this example:
volatile string[] _args = null;
public void Write() {
string[] a = new string[2];
a[0] = "arg1";
a[1] = "arg2";
_args = a;
...
}
public void Read() {
if (_args != null) {
// Under weak volatile semantics, this assert could fail!
Debug.Assert(_args[0] != null);
}
}
Under strong volatile semantics (i.e., the .NET and C# volatile semantics), a non-null value in the _args field guarantees that the elements of _args are also not null. The safe publication pattern is very useful and commonly used in practice.
* Разумеется, Write() и Read() должны выполняться в разных потоках.
Здравствуйте, Sinix, Вы писали:
S>Я никак не соображу, как это возможно на практике (рассматриваем только реализацию рантайма от МС))? Для x86/x64 с их strong memory model volatile по сути отключает оптимизации компилятора: S> S>... S> S>На итаниумах (и, подозреваю на армах) — запись всё равно volatile даже у обычных полей (оттуда жа):
Всё очень просто: модели памяти процессоров устроены сложнее, чем термин volatile в определении C#\CLR.
В частности, на x86 последовательность store\load может реордериться, если участвующие команды обращаются к разным участкам памяти. Есть несколько форм реордеринга и для store\store.
Здравствуйте, drol, Вы писали:
D>В частности, на x86 последовательность store\load может реордериться, если участвующие команды обращаются к разным участкам памяти.
Кстати, volatile спецификаций C#\CLI в этом месте аналогичен. Последовательность store\load разных volatile-полей тоже может реордериться.
1. создается объект
2. ссылка на него возвращается выполняющемуся коду
3. другой тред получает значение его поля
4. поле инициализируется значением 42
в моем представлении порядок такой — 1-4-2-3. То-есть нельзя ни в каком случае получить ссылку на еще не созданный полностью (с отработавшим ctor)
объект
Здравствуйте, drol, Вы писали:
D>Кстати, volatile спецификаций C#\CLI в этом месте аналогичен. Последовательность store\load разных volatile-полей тоже может реордериться.
Это точно справедливо для того, что генерит jit? Дело в том, что спецификация шарпа (10.5.3) тут совершенно однозначна:
A write of a volatile field is called a volatile write. A volatile write has “release semantics”; that is, it is guaranteed to happen after any memory references prior to the write instruction in the instruction sequence.
т.е. по спецификации
_a = 1; // volatile write 1
_b = 2; // volatile write 2 не может выполниться до "_a = 1"
Плюс (см цитату в предыдущем посте) Игорь утверждает, что любой volatile write сбрасывает весь кэш процессора в память, т.е. реордеринга для store тут быть не должно.
Здравствуйте, Константин Л., Вы писали:
КЛ>какое это имеет отношение к примеру Sinix'а?
Самое прямое. Показывается, что x86 может делать кучу реордерингов на ровном месте.
КЛ>Неужели возможна такая ситуация
Согласно спецификации C#\CLI — возможна. Причём даже без участия процессора, а лишь на уровне компилятора\JIT'а...
КЛ>в моем представлении порядок такой — 1-4-2-3
Вы забываете, что потоков два. И, соответственно, последовательностей операций тоже две. Вот напишите их отдельно и рядом.
КЛ> То-есть нельзя ни в каком случае получить ссылку на еще не созданный полностью (с отработавшим ctor) объект
Здравствуйте, Sinix, Вы писали:
S>Плюс (см цитату в предыдущем посте) Игорь утверждает, что любой volatile write сбрасывает весь кэш процессора в память
Если бы Вы внимательно прочли тот древний текст Игоря, то заметили бы, что "кэш" в нём это некоторый виртуальный\воображаемый механизм, предложенный автором для упрощения — "упрощения" с его точки зрения на тот момент — понимания вопроса.
В реальности же кэш не имеет никакого отношения к реордерингу. И полный комплект развлечений с оным можно получить даже на неоснащённым кэшем одноядерном процессоре.
Собственно, за прошедшее с того поста время Игорь, похоже, всё это осознал. И именно поэтому в его новой статье слова cache — и прочих ужосов — нет как класса
Здравствуйте, drol, Вы писали:
D>Здравствуйте, Константин Л., Вы писали:
КЛ>>какое это имеет отношение к примеру Sinix'а?
D>Самое прямое. Показывается, что x86 может делать кучу реордерингов на ровном месте.
Никакого
КЛ>>Неужели возможна такая ситуация
D>Согласно спецификации C#\CLI — возможна. Причём даже без участия процессора, а лишь на уровне компилятора\JIT'а...
КЛ>>в моем представлении порядок такой — 1-4-2-3
D>Вы забываете, что потоков два. И, соответственно, последовательностей операций тоже две. Вот напишите их отдельно и рядом.
КЛ>> То-есть нельзя ни в каком случае получить ссылку на еще не созданный полностью (с отработавшим ctor) объект
D>"Я плакаль" (с) не мой
Продолжай плакать дальше
D>Внутри конструктора отдайте this кому-нибудь...
Здравствуйте, Константин Л., Вы писали:
КЛ>Никакого
И это вся Ваша аргументация ? Как предсказуемо
КЛ>Этого нет в примере
Ага. Однако этот простой момент показывает, что в конструкторах нет ничего сакрального. И с точки зрения подсистемы исполнения они есть обычные методы, которые оптимизируются согласно обычным правилам.
Насколько я могу судить, модель памяти ECMA принята в Mono. На главной страничке проекта так и написано. Стало быть, если нужен полностью переносимый код, то нужно следовать советам из статьи.
Меня интересует, во всех ли продуктах Microsoft принята их собственная модель памяти? Скажем, в CompactFramework, в WinPhone? Очень хочется на это надеяться, но боюсь подложенной свиньи.
Здравствуйте, SergeyT., Вы писали: ST>дается объяснение, почему в правильной реализации синглтона с двойной проверкой (double-checked lock Singleton) нужно, чтобы статическое поле было с ключевым словом volatile.
ECMA, на который ссылается автор опиcывает модель памяти C#, где для volatile описаны следующие разрешённые перестановки:
The effect of applying volatile to fields can be summarized as follows:
First instruction Second instruction Can they be swapped?
Read Read No
Read Write No
Write Write No (The CLR ensures that write-write operations are never swapped, even without the volatile keyword)
Write Read Yes!
т.е. это касается самого компилятора, но в мультитрединге есть ещё одно действующее лицо — Процессор, он тоже делает оптимизации чтения/записи и ему совершенно наплевать на то, что там имел ввиду программист.
Пример:
public class Test
{
public volatile bool a;
public volatile bool b;
public volatile bool c;
private void ThreadFuncA()
{
a = true;
if (b)
{
c = true;
}
}
private void ThreadFuncB()
{
b = true;
if (a)
{
c = true;
}
}
В дизасме нет ничего, что бы хоть как-то гарантировало порядок выполнения.
Скрытый текст
; функция B
b = true;
00000000 push ebp
00000001 mov ebp,esp
00000003 push eax
00000004 mov dword ptr [ebp-4],ecx
00000007 cmp dword ptr ds:[0068A1C8h],0
0000000e je 00000015
00000010 call 6C86453C
00000015 mov eax,dword ptr [ebp-4]
00000018 mov byte ptr [eax+21h],1
if (a)
0000001c mov eax,dword ptr [ebp-4]
0000001f cmp byte ptr [eax+20h],0
00000023 je 0000002C
{
c = true;
00000025 mov eax,dword ptr [ebp-4]
00000028 mov byte ptr [eax+22h],1
}
}
0000002c nop
0000002d mov esp,ebp
0000002f pop ebp
00000030 ret
; Функция А
a = true;
00000000 push ebp
00000001 mov ebp,esp
00000003 push eax
00000004 mov dword ptr [ebp-4],ecx
00000007 cmp dword ptr ds:[0031A1C8h],0
0000000e je 00000015
00000010 call 6CA644B4
00000015 mov eax,dword ptr [ebp-4]
00000018 mov byte ptr [eax+20h],1
if (b)
0000001c mov eax,dword ptr [ebp-4]
0000001f cmp byte ptr [eax+21h],0
00000023 je 0000002C
{
c = true;
00000025 mov eax,dword ptr [ebp-4]
00000028 mov byte ptr [eax+22h],1
}
}
0000002c nop
0000002d mov esp,ebp
0000002f pop ebp
00000030 ret
т.е. volatile бесполезен, его недостаточно — вокруг барьеров памяти реордер запрещён и комплятору и процессору, а volatile ничего не гарантирует (процессору он реордер не запрещает).
Всё сказанное выше — личное мнение, если не указано обратное.