Я опробовал в интернете множество примеров,но они не показывают на практике,разницу работы одного и того-же кода с применением Volatile и без.Можно ли на персональном компьютере осуществить это,или разницу можно увидеть только при работе сервера с множеством клиентов,которые одновременно используют и изменяют одну и туже переменную через разные потоки?
Ответ
Ну вот вам пример.
class Program
{
static bool finish = false;
static void Main(string[] args)
{
new Thread(ThreadProc).Start();
int x = 0;
while (!finish)
{
x++;
}
}
static void ThreadProc()
{
Thread.Sleep(1000);
finish = true;
}
}
Откомпилируйте Release, запустите из командной строки (не из Visual Studio), увидите, что программа не завершается. Поменяйте код на такой:
class Program
{
static bool finish = false;
static void Main(string[] args)
{
new Thread(ThreadProc).Start();
int x = 0;
while (!Volatile.Read(ref finish)) // while (!finish)
{
x++;
}
}
static void ThreadProc()
{
Thread.Sleep(1000);
Volatile.Write(ref finish, true); // finish = true;
}
}
— этот код будет завершаться.
Идея примера честно украдена из ответа Marc Gravell
Пояснение. Многопоточные проблемы с обновлением данных между потоками возникают не только из-за оптимизации на уровне процессора (вид разных процессоров многопроцессорной системы на оперативную память может быть различным), а и из-за оптимизаций компилятора, который имеет право (это важно!) переставлять операции, если только смысл кода в каждом отдельном потоке не меняется.
В обычном случае х86-системы с достаточно сильной моделью памяти процессор таких трюков почти не делает, так что проблемы нам с вами видны не так часто, в отличие от программистов на ARM, например. А вот оптимизации компилятора, наоборот, становятся всё агрессивнее и агрессивнее, хотя и в рамках стандарта языка.
Право компилятора на изменение внутреннего смысла кода известно как as-if rule: компилятор может производить любой код, если только его побочные эффекты в каждом отдельном потоке оставались те же и в том же порядке. В частности, компилятор имеет право выбросить повторное чтение переменной, если он видит, что она в данном потоке не меняется.
Однако некоторые операции (например, Volatile.Read/Write, lock, Thread.Start/Join и т. п.) являются критическими точками, и оптимизации проводятся лишь в блоках между такими точками.
В нашем случае оптимизатор смог понять, что переменная finish с точки зрения основного потока не меняется, и выкинул повторное чтение. Имел на это полное право.
Когда мы добавили Volatile-операции, то компилятор более не имел права не перечитывать переменную, так что изменения были «замечены».
Ссылки на C# Language Specification
(раздел 3.10):
Execution of a C# program proceeds such that the side effects of each executing thread are preserved at critical execution points. [...] The execution environment is free to change the order of execution of a C# program, subject to the following constraints:
Data dependence is preserved within a thread of execution. That is, the value of each variable is computed as if all statements in the thread were executed in original program order.
Initialization ordering rules are preserved (§10.5.4 and §10.5.5).
The ordering of side effects is preserved with respect to volatile reads
and writes (§10.5.3). [...]
(раздел 10.5.3):
For non-volatile fields, optimization techniques that reorder instructions can lead to unexpected and unpredictable results in multi-threaded programs that access fields without synchronization such as that provided by the lock-statement (§8.12). These optimizations can be performed by the compiler, by the run-time system, or by hardware.
Комментариев нет:
Отправить комментарий