Работает ли нуль-условный оператор одинаково с делегатами и обычными объектами? - PullRequest
1 голос
/ 10 июля 2019

Ссылка

В настоящее время я имею дело с некоторым чувствительным к потокам кодом.

В моем коде у меня есть список объектов, которыми манипулируют два разных потока.Один поток может добавлять объекты в этот список, в то время как другой может устанавливать его на нуль.

В приведенной выше ссылке конкретно упоминается, что для делегатов:

myDelegate?.Invoke()

эквивалентно:

var handler = myDelegate;
if (handler != null)
{
    handler(…);
}

Мой вопрос такой же, как, скажем, List<>?Например:

Is:

var myList = new List<object>();    
myList?.Add(new object());

, гарантированно эквивалентный:

var myList = new List<object>();

var tempList = myList;
if (tempList != null)
{
    tempList.Add(new object());
}

?


РЕДАКТИРОВАТЬ:

Обратите внимание, что есть разница между (как работает делегат):

var myList = new List<int>();
var tempList = myList;
if (tempList != null)
{
    myList = null; // another thread sets myList to null here
    tempList.Add(1); // doesn't crash
}

И

var myList = new List<int>();
if (myList != null)
{
    myList = null; // another thread sets myList to null here
    myList.Add(1); // crashes
}

Ответы [ 4 ]

7 голосов
/ 12 июля 2019

Это тонкая проблема, требующая тщательного анализа.

Во-первых, код, заданный в вопросе, не имеет смысла, поскольку он проверяет локальную переменную, которая гарантированно не будет нулевой. Предположительно реальный код читает из нелокальной переменной, которая может быть или не быть нулевой, и может быть изменена в нескольких потоках.

Это очень опасная позиция, и я настоятельно не рекомендую вам следовать этому архитектурному решению . Найдите другой способ поделиться памятью между рабочими.

Чтобы ответить на ваш вопрос:

Первая версия вопроса: у оператора ?. та же семантика, что и у вашей версии, где вы вводите временную строку?

Да, это так. Но мы еще не закончили.

Второй вопрос, который вы не задавали, заключается в следующем: возможно ли, что компилятор C #, джиттер или ЦП приводят к тому, что версия с временным вводит дополнительное чтение? То есть мы гарантируем, что

var tempList = someListThatCouldBeNull;
if (tempList != null)
    tempList.Add(new object());

никогда не выполняется , как если бы вы написали

var tempList = someListThatCouldBeNull;
if (tempList != null) 
    someListThatCouldBeNull.Add(new object());

Вопрос о «введенных чтениях» сложен в C #, но короткая версия такова: вообще говоря, можно предположить, что чтения не будут введены таким образом.

Мы хороши? Конечно, нет. Код не является поточно-ориентированным, поскольку Add может вызываться в нескольких потоках, что является неопределенным поведением!

Предположим, мы это исправим. Все хорошо сейчас?

Нет. Нам все еще не следует доверять этому коду.

Почему бы и нет?

На оригинальном плакате не показан механизм, который гарантирует, что текущее значение someListThatCouldBeNull читается. Доступ к нему осуществляется с помощью блокировки? Это изменчиво? Введены ли барьеры памяти? Спецификация C # очень ясно показывает тот факт, что чтения могут быть произвольно перемещены назад во времени, если нет никаких специальных эффектов, таких как блокировки или летучие компоненты. Возможно, вы читаете кэшированное значение.

Точно так же мы не видели код, который выполняет запись; эти записи могут быть произвольно перенесены в будущее. Любая комбинация чтения, перенесенного в прошлое, или записи, перенесенной в будущее, может привести к считыванию «устаревшего» значения.

Теперь предположим, что мы решили эту проблему. Это решает всю проблему? Конечно, нет. Мы не знаем, сколько потоков задействовано или какие-либо из этих потоков также читают связанные переменные, и есть ли какие-либо предполагаемые ограничения порядка для этих чтений . C # не требует наличия глобально согласованного представления порядка всех операций чтения и записи! Два потока могут не соглашаться в том порядке, в котором выполнялись операции чтения и записи в переменные. То есть, если модель памяти допускает два возможных наблюдаемых порядка, для одного потока допустимо наблюдать один, а для другого потока - другой. Если логика вашей программы неявно зависит от единственного наблюдаемого порядка чтения и записи, значит, ваша программа ошибочна .

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

Так что же вам делать?

  • Если вы можете: прекратить использование потоков . Найдите другой способ справиться с асинхронностью.
  • Если вы не можете сделать это, использует потоки в качестве рабочих, которые решают проблему, а затем возвращаются в пул . Трудно понять, что два потока одновременно работают с одной и той же памятью. Отключить один поток, вычислить что-то и вернуть значение, когда это будет сделано, гораздо проще понять, и вы можете ...
  • ... используйте параллельную библиотеку задач или другой инструмент, предназначенный для правильного управления связью между потоками.
  • Если вы не можете этого сделать, попытайтесь изменить как можно меньше переменных .Не устанавливайте переменную в нуль.Если вы заполняете список, инициализируйте список потокобезопасным списком типа один раз, а затем читайте только из этой переменной.Пусть объект списка обрабатывает проблемы с потоками для вас.
2 голосов
/ 10 июля 2019

В этом ответе Эрик Липперт подтверждает, что временная переменная используется во всех случаях, что предотвратит «?».Оператор вызывает исключение NullReferenceException или доступ к двум различным объектам.Однако существует множество других факторов, которые могут сделать этот код не защищенным от потоков, см. ответ Эрика .

UPD: для подтверждения утверждения, что временная переменная не создана: нетнеобходимо ввести временную переменную для локальной переменной.Однако, если вы попытаетесь получить доступ к чему-то, что может быть изменено, создается переменная.Используя тот же SharpLab со слегка измененным кодом, мы получаем:

using System;
using System.Collections.Generic;

public class C {
    public List<Object> mList;

    public void M() {
        this.mList?.Add(new object());
    }
}

становится

public class C
{
    public List<object> mList;

    public void M()
    {
        List<object> list = mList;
        if (list != null)
        {
            list.Add(new object());
        }
    }
}
2 голосов
/ 10 июля 2019

Ответ - да.

var myList = new List<object>();    
myList?.Add(new object());

Компилируется в следующее ( как видно здесь )

List<object> list = new List<object>();
if (list != null)
{
    list.Add(new object());
}
0 голосов
/ 10 июля 2019

Да, они одинаковы.Вы также можете увидеть базовый IL ниже, сгенерированный Ildasm :

public void M()
{
    var myList = new List<object>();
    myList?.Add(new object());
}

Это будет:

.method public hidebysig instance void  M() cil managed
{
  // Code size       25 (0x19)
  .maxstack  2
  .locals init (class [System.Collections]System.Collections.Generic.List`1<object> V_0)
  IL_0000:  nop
  IL_0001:  newobj     instance void class [System.Collections]System.Collections.Generic.List`1<object>::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  brtrue.s   IL_000c
  IL_000a:  br.s       IL_0018
  IL_000c:  ldloc.0
  IL_000d:  newobj     instance void [System.Runtime]System.Object::.ctor()
  IL_0012:  call       instance void class [System.Collections]System.Collections.Generic.List`1<object>::Add(!0)
  IL_0017:  nop
  IL_0018:  ret
} // end of method C::M

И:

public void M2()
{
    List<object> list = new List<object>();
    if (list != null)
    {
        list.Add(new object());
    }
}

Это будет:

.method public hidebysig instance void  M2() cil managed
{
  // Code size       30 (0x1e)
  .maxstack  2
  .locals init (class [System.Collections]System.Collections.Generic.List`1<object> V_0,
           bool V_1)
  IL_0000:  nop
  IL_0001:  newobj     instance void class [System.Collections]System.Collections.Generic.List`1<object>::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  ldnull
  IL_0009:  cgt.un
  IL_000b:  stloc.1
  IL_000c:  ldloc.1
  IL_000d:  brfalse.s  IL_001d
  IL_000f:  nop
  IL_0010:  ldloc.0
  IL_0011:  newobj     instance void [System.Runtime]System.Object::.ctor()
  IL_0016:  callvirt   instance void class [System.Collections]System.Collections.Generic.List`1<object>::Add(!0)
  IL_001b:  nop
  IL_001c:  nop
  IL_001d:  ret
} // end of method C::M2
...