Является ли оценка оператора "switch" поточно-ориентированной? - PullRequest
25 голосов
/ 11 июля 2011

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

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-битная операция не является атомарной, так что сначала у переменной будет нулевое нижнее слово, а затем верхнее значение (или наоборот).

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

Ответы [ 4 ]

16 голосов
/ 11 июля 2011

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

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

long y = Interlocked.Read(ref x);
switch (y)
{
    // ...
}
12 голосов
/ 11 июля 2011

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

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

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

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

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

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

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

long myLocal = x;
switch(myLocal)
{
}

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

2 голосов
/ 11 июля 2011

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

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

Таким образом, у вас нет потенциального условия гонки, которое вы указали, где выполняется сравнение, изменяется значение, выполняется второе сравнение и т. Д., Потому что выполняется только одна проверка. Что касается того, является ли это полностью потокобезопасным - я не предполагал бы так.

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

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

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

1 голос
/ 11 июля 2011

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

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

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

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