TLDR: Ваш код является поточно-ориентированным. Вы правы, опасаясь, что чтение поля _status
через свойство IsChanged
может привести к устареванию значений; однако этого не произойдет в вашем методе Clear
, поскольку другие существующие операторы lock
уже защищают от этого.
В многопоточном программировании часто встречаются две связанные концепции: взаимный исключение и согласованность памяти . Взаимное исключение включает в себя разграничение критических разделов вашего кода, так что только один поток может выполнить их в любое время. Это то, что lock
предназначено в первую очередь для достижения.
Согласованность памяти является более эзотерической c topi c и имеет дело с порядком, в котором записи в ячейки памяти один поток может отображаться для чтения из тех же областей памяти другими потоками. Операции с памятью могут выглядеть переупорядоченными между потоками из соображений производительности, таких как лучшее использование локальных кэшей. Без синхронизации потоки могут считывать устаревшие значения областей памяти, которые были обновлены другими потоками - запись, по-видимому, была выполнена после чтения с точки зрения потока читателя. Чтобы избежать такого переупорядочения, программист может ввести в свой код барьеры памяти , что предотвратит перемещение операций с памятью. На практике такие барьеры памяти обычно генерируются неявно только в результате других операций с потоками. Например, оператор lock
создает барьер памяти как при входе, так и при выходе.
Однако создание барьеров памяти с помощью оператора lock
является только побочным эффектом, поскольку основная цель lock
для обеспечения взаимного исключения. Это может привести к путанице, когда программисты сталкиваются с другими ситуациями, когда взаимное исключение также достигается, но барьеры памяти не создаются неявно. Одной из таких ситуаций является чтение и запись полей шириной до 32 или 64 бит, которые гарантированно будут иметь атомы c на архитектурах такой ширины. Чтение или запись логического значения по своей сути поточно-ориентированно - вам никогда не понадобится lock
для принудительного взаимного исключения при этом, поскольку никакой другой параллельный поток не может «испортить» значение. Однако чтение или запись логического значения без lock
означает, что не создаются барьеры памяти, поэтому могут возникнуть устаревшие значения.
Давайте применим это обсуждение к урезанной версии вашего кода:
// Thread A:
_testerStatus = true; // tester.SetStatusToTrue();
_testerReleased = true; // testClassManager.ReleaseTester(tester.Id);
// Thread B:
if (_testerReleased) // foreach (var tester in _releasedTesters)
Console.WriteLine($"Status: {_testerStatus}"); // if (!tester.Value.IsChanged)
Возможно ли для вышеуказанной программы выводить false
? В этом случае да. Это пример classi c, обсуждаемый в разделе Неблокирующая синхронизация (рекомендуемое чтение). Ошибка может быть исправлена путем добавления барьеров памяти (явно, как показано ниже, или неявно, как описано далее ниже):
// Thread A:
_testerStatus = true; // tester.SetStatusToTrue();
Thread.MemoryBarrier(); // Barrier 1
_testerReleased = true; // testClassManager.ReleaseTester(tester.Id);
Thread.MemoryBarrier(); // Barrier 2
// Thread B:
Thread.MemoryBarrier(); // Barrier 3
if (_testerReleased) // foreach (var tester in _releasedTesters)
{
Thread.MemoryBarrier(); // Barrier 4
Console.WriteLine($"Status: {_testerStatus}"); // if (!tester.Value.IsChanged)
}
В вашем коде ваш метод ReleaseTester
имеет внешний lock
, который неявно генерирует эквивалент барьеров 1 и 2 выше. Точно так же ваш Clear
метод также имеет внешний lock
, поэтому он генерирует эквивалент Barrier 3. Ваш код не генерирует эквивалент Barrier 4; однако я считаю, что этот барьер в вашем случае не нужен, поскольку взаимное исключение, установленное lock
, влечет за собой то, что барьер 2 должен быть выполнен до барьера 3, если тестер был освобожден.