Является ли оценка оператора switch потокобезопасной?

Рассмотрим следующий пример кода:

class MyClass
{
    public long x;

    public void DoWork()
    {
        switch (x)
        {
            case 0xFF00000000L:
                // do whatever...
                break;

            case 0xFFL:
                // do whatever...
                break;

            default:
                //notify that something going wrong
                throw new Exception();
        }
    }
}

Забудьте о бесполезности фрагмента: я сомневаюсь в поведении оператора switch.

Предположим, что поле x может иметь только два значения: 0xFF00000000L или 0xFFL. Переключатель выше не должен попадать в вариант «по умолчанию».

Теперь представьте, что один поток выполняет переключатель с «x», равным 0xFFL, поэтому первое условие не будет совпадать. В то же время другой поток изменяет переменную «x» на 0xFF00000000L. Мы знаем, что 64-битная операция не является атомарной, поэтому в переменной сначала будет обнулено нижнее двойное слово, а затем — верхнее (или наоборот).

Если второе условие в переключателе будет выполнено, когда «х» равно нулю (т. е. во время нового назначения), попадем ли мы в нежелательный случай «по умолчанию»?


person Mario Vernari    schedule 11.07.2011    source источник
comment
У меня нет фактических фактов, подтверждающих мое мнение, но я на 99,9% уверен, что это не потокобезопасно.   -  person Dyppl    schedule 11.07.2011
comment
Переключение не является специфическим, любой оператор, использующий переменную, может наблюдать ее во временном несогласованном состоянии.   -  person Hans Passant    schedule 11.07.2011
comment
Да, очень изворотливый. Всякий раз, когда я вижу, что ко мне приближается такой класс, я пытаюсь создать экземпляр для каждого вызывающего потока и, таким образом, избежать проблемы (да, не всегда возможно, в зависимости от функциональности и данных).   -  person Martin James    schedule 11.07.2011


Ответы (4)


Вы на самом деле публикуете два вопроса.

Это потокобезопасно?

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

Вы когда-нибудь попадете в состояние переключателя по умолчанию?

Теоретически вы могли бы, так как обновление 64 бит не является атомарной операцией, и, таким образом, теоретически вы могли бы перейти в середину задания и получить смешанный результат для x, как вы указываете. Статистически это будет происходить нечасто, но рано или поздно это произойдет.

Но сам переключатель является потокобезопасным, то, что не является потокобезопасным, читается и записывает по 64-битным переменным (в 32-битной ОС).

Представьте, что вместо switch(x) у вас есть следующий код:

long myLocal = x;
switch(myLocal)
{
}

теперь переключение выполняется через локальную переменную, и, таким образом, оно полностью потокобезопасно. Проблема, конечно же, в myLocal = x чтении и его конфликте с другими присваиваниями.

person Jorge Córdoba    schedule 11.07.2011
comment
Ваш код ...imagine вместо... в значительной степени совпадает с тем, что компилятор генерирует для блока switch в любом случае. Выражение-переключатель — в данном случае загрузка из поля x — выполняется только один раз, а результат сохраняется в локальном; этот локальный затем используется для блока переключателей. - person LukeH; 11.07.2011
comment
Может быть, это потому, что я не очень хорошо говорю по-английски, но я знаю, что этот фрагмент далек от потокобезопасности. Потокобезопасность была сосредоточена только на операторе switch. Заглянув с IL-disass, вижу, что есть копия значения, потом сравниваю с каждым тестом. Это очень похоже на ассемблерный код. - person Mario Vernari; 11.07.2011
comment
@Mario Vernari: вы доказали, что компилятор может генерировать такой код, но не всегда. На компиляторы в реальном мире влияют такие факторы, как количество операторов switch, их значение, количество используемых переменных и связанное с этим нехватка памяти и т. д. Таким образом, дизассемблирование, которое вы видите, не является репрезентативным для всех ситуаций. - person MSalters; 11.07.2011
comment
@MSalters: выражение-переключатель оценивается только один раз в соответствии с ECMA C#. spec (раздел 15.7.2, если вам интересно). - person LukeH; 11.07.2011
comment
@LukeH: Это действительно правильный способ показать, что компилятор должен делать, а не просто показать, что он может сделать. Но обратите внимание, что 10.10 Execution Order дает довольно много свободы для изменения порядка вычисления энергонезависимых выражений. Вычисление выражения энергонезависимого переключателя не является побочным эффектом. - person MSalters; 11.07.2011
comment
@Mario Vernari: если вы включите оптимизацию, компилятор может решить не кэшировать переменную; или какая-то будущая версия может добавить эту оптимизацию. Даже если пока работает, если не гарантируется, что компилятор закэширует переменную, то это не так. - person Lie Ryan; 11.07.2011
comment
@LukeH Итак, вы говорите, что этот ответ неверен; случай по умолчанию никогда не сработает, верно? - person Asad Saeeduddin; 19.04.2014
comment
@Asad: Нет, я не об этом. Если вам не повезло, случай по умолчанию может сработать, потому что начальное 64-битное чтение из поля x не гарантирует потокобезопасности. - person LukeH; 24.04.2014

Да, сам оператор switch, как показано в вашем вопросе, является потокобезопасным. Значение поля x загружается один раз в (скрытую) локальную переменную, и эта локальная переменная используется для блока switch.

Что небезопасно, так это первоначальная загрузка поля x в локальную переменную. 64-битные операции чтения не гарантируют атомарность, поэтому при этом вы можете получить устаревшие и/или поврежденные операции чтения. точка. Это можно легко решить с помощью Interlocked.Read или аналогичного для явного прочитайте значение поля в локальный потокобезопасным способом:

long y = Interlocked.Read(ref x);
switch (y)
{
    // ...
}
person LukeH    schedule 11.07.2011

Оператор switch C# не оценивается как ряд условий if (как это может быть в VB). C# эффективно создает хеш-таблицу меток для перехода на основе значения объекта и сразу переходит к правильной метке, а не выполняет итерацию по каждому условию по очереди и оценивает его.

Вот почему оператор switch C# не ухудшает свою скорость по мере увеличения числа случаев. И именно поэтому C# более ограничивает то, с чем вы можете сравнивать в случаях переключения, чем VB, в котором вы можете, например, делать диапазоны значений.

Следовательно, у вас нет потенциального состояния гонки, о котором вы заявили, когда выполняется сравнение, значение изменяется, выполняется второе сравнение и т. д., потому что выполняется только одна проверка. Что касается того, является ли он полностью потокобезопасным, я бы так не предположил.

Покопайтесь с рефлектором, просмотрев оператор switch C# в IL, и вы увидите, что происходит. Сравните его с оператором switch из VB, который включает диапазоны значений, и вы увидите разницу.

Прошло уже несколько лет с тех пор, как я смотрел его, так что, возможно, что-то изменилось немного...

Подробнее о поведении оператора switch см. здесь: Есть ли существенная разница между использованием if/else и switch-case в С#?

person Niall Connaughton    schedule 11.07.2011
comment
В некоторых ответах на этот вопрос утверждается, что переключатель может быть реализован как серия операторов if-else, таблица переходов или что-то еще. Хотя сам понятия не имею... - person eran; 11.07.2011
comment
@eran хорошо, ссылка не работает - person V4Vendetta; 11.07.2011
comment
@Niall, @eran: Да, есть определенный порог, ниже которого компилятор просто использует набор условий if. - person LukeH; 11.07.2011
comment
Я думаю, что стандарт ECMA никоим образом не определяет, как операторы switch должны быть скомпилированы в инструкции CLR. JIT-операторы могут использовать эвристику для оптимизации этого, а таблица переходов может использоваться только в определенных случаях (более очевидным случаем являются большие коммутаторы). - person sehe; 11.07.2011
comment
@V4Vendetta спасибо, не знаю, как это туда попало... Правильная ссылка: оператор переключения C# ограничения - почему? - person eran; 11.07.2011

Как вы уже догадались, оператор switch не является потокобезопасным и в определенных сценариях может завершиться ошибкой.

Кроме того, использование lock в вашей переменной экземпляра также не будет работать, потому что оператор lock ожидает object, что приводит к тому, что ваша переменная экземпляра будет помещена в коробку. Каждый раз, когда переменная экземпляра помещается в коробку, будет создаваться новая переменная в упаковке, что делает lock фактически бесполезным.

На мой взгляд у вас есть несколько вариантов решения этого вопроса.

  1. Используйте lock для частной переменной экземпляра любого ссылочного типа (object выполнит эту работу)
  2. Используйте ReaderWriterLockSlim, чтобы позволить нескольким потокам читать переменную экземпляра, но только одному потоку записывать переменную экземпляра за раз.
  3. Атомарно сохраните значение переменной экземпляра в локальной переменной в вашем методе (например, используя Interlocked.Read или Interlocked.Exchange) и выполните switch для локальной переменной. Обратите внимание, что таким образом вы можете использовать старое значение для switch. Вы должны решить, может ли это вызвать проблемы в вашем конкретном случае использования.
person Florian Greinacher    schedule 11.07.2011