Является ли нулевой коалесцирующий поток операторов безопасным?

Итак, это мясо вопроса: может ли 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>();
    }
}

Это не похоже на потокобезопасность в отношении, о котором вы упомянули.