Является ли нулевой коалесцирующий поток операторов безопасным?
Итак, это мясо вопроса: может ли Foo.Bar когда-либо возвращать null? Чтобы уточнить, может ли "_bar" быть установлено значение null после того, как оно будет оценено как непустое и до того, как оно будет возвращено?
public class Foo
{
Object _bar;
public Object Bar
{
get { return _bar ?? new Object(); }
set { _bar = value; }
}
}
Я знаю, что использование следующего метода get небезопасно и может возвращать нулевое значение:
get { return _bar != null ? _bar : new Object(); }
UPDATE:
Еще один способ взглянуть на ту же проблему, этот пример может быть более ясным:
public static T GetValue<T>(ref T value) where T : class, new()
{
return value ?? new T();
}
И снова спрашивая, может ли GetValue (...) когда-либо возвращать null? В зависимости от вашего определения это может быть или не быть потокобезопасным... Я предполагаю, что правильная постановка задачи задается вопросом, является ли это атомной операцией по значению... Дэвид Яу определил вопрос лучше всего, сказав, что указанная выше функция эквивалентна к следующему:
public static T GetValue<T>(ref T value) where T : class, new()
{
T result = value;
if (result != null)
return result;
else
return new T();
}
Ответы
Ответ 1
Нет, это не безопасно для потоков.
IL для вышеуказанного компилируется в:
.method public hidebysig specialname instance object get_Bar() cil managed
{
.maxstack 2
.locals init (
[0] object CS$1$0000)
L_0000: nop
L_0001: ldarg.0
L_0002: ldfld object ConsoleApplication1.Program/MainClass::_bar
L_0007: dup
L_0008: brtrue.s L_0010
L_000a: pop
L_000b: newobj instance void [mscorlib]System.Object::.ctor()
L_0010: stloc.0
L_0011: br.s L_0013
L_0013: ldloc.0
L_0014: ret
}
Это эффективно загружает поле _bar
, затем проверяет его существование и прыгает с конца. Синхронизация отсутствует, и поскольку это несколько инструкций IL, возможно, чтобы вторичный поток вызывал условие гонки, в результате чего возвращаемый объект отличался от одного набора.
Гораздо лучше обрабатывать ленивое создание через Lazy<T>
. Это обеспечивает потокобезопасный, ленивый шаблон создания. Конечно, вышеприведенный код не делает ленивого экземпляра (скорее, возвращая новый объект каждый раз до некоторого времени, когда установлен _bar
), но я подозреваю, что ошибка, а не предполагаемое поведение.
Кроме того, Lazy<T>
затрудняет настройку.
Чтобы дублировать описанное выше поведение поточно-безопасным способом, требуется явная синхронизация.
Что касается вашего обновления:
Геттер для свойства Bar никогда не может вернуть значение null.
Посмотрев на IL выше, он _bar
(через ldfld), затем проверяет, не является ли этот объект недействительным, используя brtrue.s. Если объект не является нулевым, он перескакивает, копирует значение _bar
из стека выполнения в локальный через stloc.0 и возвращает - возврат _bar
с реальным значением.
Если _bar
не удалось установить, то он вытащит его из стека выполнения и создаст новый объект, который затем будет сохранен и возвращен.
В любом случае предотвращается возврат значения null
. Однако, опять же, я бы не рассматривал этот поточно-безопасный вообще, так как возможно, что вызов, который должен выполняться одновременно с вызовом get, может привести к возврату различных объектов, и это состояние гонки, какой объект экземпляр возвращается (заданное значение или новый объект).
Ответ 2
Я бы не использовал слово "thread safe", чтобы ссылаться на это. Вместо этого я задал бы вопрос, какой из них такой же, как оператор нулевой коалесценции?
get { return _bar != null ? _bar : new Object(); }
или
get
{
Object result = _bar;
if(result == null)
{
result = new Object();
}
return result;
}
Из чтения других ответов, похоже, он компилируется в эквивалент ко второму, а не по первому. Как вы отметили, первый может вернуть нуль, но второй никогда не будет.
Является ли этот поток безопасным? Технически, нет. После прочтения _bar
другой поток может изменить _bar
, и получатель вернет значение, устаревшее. Но из того, как вы задали вопрос, я думаю, что это то, что вы ищете.
Изменить: вот способ сделать это, чтобы избежать всей проблемы. Поскольку value
- локальная переменная, она не может быть изменена за кулисами.
public class Foo
{
Object _bar = new Object();
public Object Bar
{
get { return _bar; }
set { _bar = value ?? new Object(); }
}
}
Изменить 2:
Здесь IL, которую я вижу из компиляции Release, с моей интерпретацией IL.
.method public hidebysig specialname instance object get_Bar_NullCoalesce() cil managed
{
.maxstack 8
L_0000: ldarg.0 // Load argument 0 onto the stack (I don't know what argument 0 is, I don't understand this statement.)
L_0001: ldfld object CoalesceTest::_bar // Loads the reference to _bar onto the stack.
L_0006: dup // duplicate the value on the stack.
L_0007: brtrue.s L_000f // Jump to L_000f if the value on the stack is non-zero.
// I believe this consumes the value on the top of the stack, leaving the original result of ldfld as the only thing on the stack.
L_0009: pop // remove the result of ldfld from the stack.
L_000a: newobj instance void [mscorlib]System.Object::.ctor()
// create a new object, put a reference to it on the stack.
L_000f: ret // return whatever on the top of the stack.
}
Вот что я вижу из других способов сделать это:
.method public hidebysig specialname instance object get_Bar_IntermediateResultVar() cil managed
{
.maxstack 1
.locals init (
[0] object result)
L_0000: ldarg.0
L_0001: ldfld object CoalesceTest::_bar
L_0006: stloc.0
L_0007: ldloc.0
L_0008: brtrue.s L_0010
L_000a: newobj instance void [mscorlib]System.Object::.ctor()
L_000f: stloc.0
L_0010: ldloc.0
L_0011: ret
}
.method public hidebysig specialname instance object get_Bar_TrinaryOperator() cil managed
{
.maxstack 8
L_0000: ldarg.0
L_0001: ldfld object CoalesceTest::_bar
L_0006: brtrue.s L_000e
L_0008: newobj instance void [mscorlib]System.Object::.ctor()
L_000d: ret
L_000e: ldarg.0
L_000f: ldfld object CoalesceTest::_bar
L_0014: ret
}
В IL очевидно, что он дважды читает поле _bar
с помощью оператора trinary, но только один раз с нулевым коалесцированием и промежуточным результатом var. Кроме того, IL метода нулевого коалесценции очень близок к методу промежуточного результата var.
И вот источник, который я использовал для их создания:
public object Bar_NullCoalesce
{
get { return this._bar ?? new Object(); }
}
public object Bar_IntermediateResultVar
{
get
{
object result = this._bar;
if (result == null) { result = new Object(); }
return result;
}
}
public object Bar_TrinaryOperator
{
get { return this._bar != null ? this._bar : new Object(); }
}
Ответ 3
Геттер никогда не вернет null.
Это происходит потому, что когда чтение выполняется над переменной (_bar
), выражение оценивается, и полученный объект (или нуль) затем "свободен" от переменной (_bar
). Это результат этой первой оценки, которая затем "передается" оператору коалесценции. (См. Reed хороший ответ для IL.)
Однако это не является потокобезопасным, и назначение может быть легко потеряно по той же причине, что и выше.
Ответ 4
Отражатель говорит "нет":
List<int> l = null;
var x = l ?? new List<int>();
Скомпилируется:
[STAThread]
public static void Main(string[] args)
{
List<int> list = null;
if (list == null)
{
new List<int>();
}
}
Это не похоже на потокобезопасность в отношении, о котором вы упомянули.