Почему ?.Invoke() считается потокобезопасным с делегатами?
Как написано в документации, PropertyChanged?.Invoke() превращается в
var handler = this.PropertyChanged;
if (handler != null)
{
handler(…);
}
Что мешает другому треду в момент между успешной проверкой (handler != null) и запуском handler'а сделать
handler -= PropertyChanged;
и получить NullReferenceException? Почему это считается thread-safe?
Ответы (2 шт):
Приведённый фрагмент - это описание реализации конструкции ?.Invoke(), то есть то, как это выглядит на CIL. То есть нигде в коде не существует переменной handler.
После того, как мы получили указатель (он ложится на стек), манипуляции с делегатом где-либо уже не предотвратят вызов метода по этому указателю. Строго говоря, операция дублирования значения на стеке аналогична созданию локальной переменной, поэтому код вполне отвечает реализации.
Вот пример, который показывает, как это работает:
using System;
using System.Threading.Tasks;
static class Programm {
class A {
int _p;
public Action PropertyChanged;
public int Psafe {
get { return _p; }
set {
_p = value;
Task.Run(() => {
var handler = PropertyChanged;
if (handler != null) {
Task.Delay(2000).Wait();
Console.WriteLine(handler.Equals(PropertyChanged)); // false
// показывает, что PropertyChanged уже не тот,
// который в handler
handler();
Console.WriteLine(_p);
}
});
}
}
public int Punsafe {
get { return _p; }
set {
_p = value;
Task.Run(() => {
// это нерекомендуемый вариант
if (PropertyChanged != null) {
Task.Delay(2000).Wait();
try { PropertyChanged(); }
catch { Console.WriteLine("Error occured"); }
Console.WriteLine(_p);
}
});
}
}
}
static void B() {
Console.WriteLine("Method B");
}
static void Main(string[] args) {
A a = new A();
Console.WriteLine("Safe test");
a.PropertyChanged += B;
a.Psafe = 3;
Console.WriteLine("Property set to 3");
Task.Delay(1000).Wait();
a.PropertyChanged -= B;
Console.WriteLine("Main 1");
Task.Delay(2000).Wait();
Console.WriteLine("Main 2");
Console.WriteLine("Unsafe test");
a.PropertyChanged += B;
a.Punsafe = 5;
Console.WriteLine("Property set to 5");
Task.Delay(1000).Wait();
a.PropertyChanged -= B;
Console.WriteLine("Main 3");
Task.Delay(2000).Wait();
Console.WriteLine("Main 4");
}
}
-->
Safe test
Property set to 3
Main 1
Method B
3
Main 2
Unsafe test
Property set to 5
Main 3
Error occured
5
Main 4
?. - это просто упрощённая проверка на null при доступе к члену класса, синтаксический сахар. А документация объясняет, что реализован этот оператор потокобезопасным способом, что работает как при вызове делегатов, так и вообще при доступе к любому члену: "The ?. operator evaluates its left-hand operand no more than once, guaranteeing that it cannot be changed to null after being verified as non-null" (над текстом по ссылке в начале вопроса).
P. S. В данном примере вместо Task.Delay(...).Wait() можно использовать Thread.Sleep(...) (но нет необходимости), но при работе в однопоточном окружении (STA - Single Threaded Apartment, как пример - WinForms) Thread.Sleep(...) будет полность блокировать единственный поток выполнения формы (никакие события обрабатываться не будут), поэтому используйте await Task.Delay(...) (о возможных конфликтах придётся позаботиться явно).
Попутно выяснилась одна интересная особенность. Делегат с его списком вызова не похож на обычный список, который может существовать пустым. Если у делегата нет ни одного подписчика, то его значение null.
Всё просто, делегат сам по своей природе потокобезопасный, потому что он немутабельный. Присваивание его в переменную дает гарантию, что никаких изменений с сохранённым делегатом не произойдёт.
Когда вы присвоили делегат в переменную, а кто-то в другом потоке добавил или убрал обработчик события из Invokation List этого делегата, то будет создан новый делегат, а тот что сохранён в переменную не будет изменён.
Так как возник спор по поводу этого ответа в комментариях, добавляю, что конструкция ?. сама по себе не обеспечивает потокобезопасноть при работе с объектом, который расположен по проверяемой ссылке. В данном конкретном случае потокобезопасность достигается именно природой самого делегата.