Подтвердить что ты не робот

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

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

4b9b3361

Ответ 1

На самом деле вы отправляете два вопроса.

Является ли он потокобезопасным?

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

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

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

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

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

long myLocal = x;
switch(myLocal)
{
}

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

Ответ 2

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

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

long y = Interlocked.Read(ref x);
switch (y)
{
    // ...
}

Ответ 3

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

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

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

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

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

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

Ответ 4

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

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

По-моему, у вас есть несколько вариантов решения этой проблемы.

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