Должен ли оператор возврата находиться внутри или вне блокировки?

145

Я только что понял, что в каком-то месте моего кода оператор return находится внутри замка, а иногда и снаружи. Какой из них лучше?

1)

void example()
{
    lock (mutex)
    {
    //...
    }
    return myData;
}

2)

void example()
{
    lock (mutex)
    {
    //...
    return myData;
    }

}

Какой мне использовать?

Патрик Дежарден
источник
Как насчет запуска Reflector и сравнения IL ;-).
Pop Catalin
6
@Pop: готово - ни то, ни другое не лучше в терминах IL - применяется только стиль C #
Marc Gravell
1
Очень интересно, вау, я кое-что узнал сегодня!
Pokus
@PopCatalin Мне жаль спрашивать об этом, но что такое "IL" и Reflector?
Sunburst275,

Ответы:

195

По сути, что-то делает код проще. Единая точка выхода - хороший идеал, но я бы не стал искажать код только для того, чтобы добиться этого ... И если альтернативой является объявление локальной переменной (вне блокировки), ее инициализация (внутри блокировки) и затем вернуть его (вне блокировки), тогда я бы сказал, что простой "return foo" внутри блокировки намного проще.

Чтобы показать разницу в IL, давайте закодируем:

static class Program
{
    static void Main() { }

    static readonly object sync = new object();

    static int GetValue() { return 5; }

    static int ReturnInside()
    {
        lock (sync)
        {
            return GetValue();
        }
    }

    static int ReturnOutside()
    {
        int val;
        lock (sync)
        {
            val = GetValue();
        }
        return val;
    }
}

(обратите внимание, что я с удовольствием утверждаю, что ReturnInsideэто более простой / чистый бит C #)

И посмотрите на IL (режим выпуска и т. Д.):

.method private hidebysig static int32 ReturnInside() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 CS$1$0000,
        [1] object CS$2$0001)
    L_0000: ldsfld object Program::sync
    L_0005: dup 
    L_0006: stloc.1 
    L_0007: call void [mscorlib]System.Threading.Monitor::Enter(object)
    L_000c: call int32 Program::GetValue()
    L_0011: stloc.0 
    L_0012: leave.s L_001b
    L_0014: ldloc.1 
    L_0015: call void [mscorlib]System.Threading.Monitor::Exit(object)
    L_001a: endfinally 
    L_001b: ldloc.0 
    L_001c: ret 
    .try L_000c to L_0014 finally handler L_0014 to L_001b
} 

method private hidebysig static int32 ReturnOutside() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 val,
        [1] object CS$2$0000)
    L_0000: ldsfld object Program::sync
    L_0005: dup 
    L_0006: stloc.1 
    L_0007: call void [mscorlib]System.Threading.Monitor::Enter(object)
    L_000c: call int32 Program::GetValue()
    L_0011: stloc.0 
    L_0012: leave.s L_001b
    L_0014: ldloc.1 
    L_0015: call void [mscorlib]System.Threading.Monitor::Exit(object)
    L_001a: endfinally 
    L_001b: ldloc.0 
    L_001c: ret 
    .try L_000c to L_0014 finally handler L_0014 to L_001b
}

Так что на уровне IL они [дайте-ка некоторые имена] идентичны (я кое-что узнал ;-p). Таким образом, единственное разумное сравнение - это (в высшей степени субъективный) закон локального стиля кодирования ... Я предпочитаю ReturnInsideпростоту, но я бы тоже не волновался.

Марк Гравелл
источник
16
Я использовал (бесплатный и отличный) .NET Reflector Red Gate (был: .NET Reflector Латца Рёдера), но ILDASM тоже подойдет.
Марк Грейвелл
1
Одним из самых мощных аспектов Reflector является то, что вы можете фактически дизассемблировать IL на предпочитаемый вами язык (C #, VB, Delphi, MC ++, Chrome и т. Д.)
Марк Гравелл
3
В вашем простом примере IL остается неизменным, но это, вероятно, потому, что вы возвращаете только постоянное значение ?! Я считаю, что для реальных сценариев результат может отличаться, и параллельные потоки могут создавать проблемы друг для друга, изменяя значение до его возврата, если оператор return находится за пределами блока блокировки. Опасно!
Торбьёрн
@MarcGravell: Я только что наткнулся на ваш пост, обдумывая то же самое, и даже после прочтения вашего ответа я все еще не уверен в следующем: есть ли ЛЮБЫЕ обстоятельства, при которых использование внешнего подхода может нарушить логику потоковой безопасности. Я спрашиваю об этом, так как предпочитаю единую точку возврата и не «ЧУВСТВУЮ» в ее потокобезопасности. Хотя, если ИЛ такой же, мои опасения в любом случае не должны быть сняты.
Raheel Khan
1
@RaheelKhan нет, нет; они одинаковые. На уровне IL нельзя находиться ret внутри .tryрегиона.
Марк Грейвелл
43

Это не имеет значения; они оба переведены компилятором в одно и то же.

Чтобы уточнить, либо эффективно переводится во что-то со следующей семантикой:

T myData;
Monitor.Enter(mutex)
try
{
    myData= // something
}
finally
{
    Monitor.Exit(mutex);
}

return myData;
Грег Бич
источник
2
Что ж, это верно в отношении try / finally - однако для возврата за пределы блокировки по-прежнему требуются дополнительные локальные переменные, которые нельзя оптимизировать, и требуется больше кода ...
Марк Гравелл
3
Вы не можете вернуться из блока попытки; он должен заканчиваться операционным кодом ".leave". Таким образом, испускаемый CIL должен быть одинаковым в любом случае.
Грег Бич
3
Вы правы - я только что посмотрел Ил (см. Обновленный пост). Я кое-что узнал ;-p
Marc Gravell
2
Круто, к сожалению, я научился мучительным часам, пытаясь испустить коды операций .ret в блоках try, и когда CLR отказывается загружать мои динамические методы :-(
Грег Бич
Я могу относиться; Я сделал довольно много Reflection.Emit, но я ленив; если я в чем-то не уверен, я пишу репрезентативный код на C #, а затем смотрю на IL. Но удивительно, как быстро вы начинаете мыслить в терминах IL (то есть упорядочивать стек).
Марк Гравелл
29

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

Рикардо Вильямил
источник
4
Это верно, похоже, что другие респонденты упускают этот момент. Сделанные ими простые образцы могут давать один и тот же IL, но это не так для большинства реальных сценариев.
Торбьёрн
4
Я удивлен, что другие ответы не говорят об этом
Акшат Агарвал
5
В этом примере они говорят об использовании переменной стека для хранения возвращаемого значения, то есть только оператора возврата за пределами блокировки и, конечно же, объявления переменной. У другого потока должен быть другой стек, и поэтому он не может причинить вреда, верно?
Гильермо Руффино,
3
Я не думаю, что это верный момент, поскольку другой поток мог бы обновить значение между вызовом возврата и фактическим присвоением возвращаемого значения переменной в основном потоке. Возвращаемое значение нельзя изменить или гарантировать согласованность с текущим фактическим значением в любом случае. Правильно?
Урош Йоксимович
Это неверный ответ. Другой поток не может изменить локальную переменную. Локальные переменные хранятся в стеке, и каждый поток имеет свой собственный стек. Кстати, размер стека потока по умолчанию составляет 1 МБ .
Теодор Зулиас
5

Если вы думаете, что замок снаружи выглядит лучше, но будьте осторожны, если вы в конечном итоге измените код на:

return f(...)

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

Роб Уокер
источник
5

Это зависит,

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

Обычно переменная mydata является локальной переменной. Я люблю объявлять локальные переменные при их инициализации. У меня редко есть данные для инициализации возвращаемого значения за пределами моей блокировки.

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

void example() { 
    int myData;
    lock (foo) { 
        myData = ...;
    }
    return myData
}

vs.

void example() { 
    lock (foo) {
        return ...;
    }
}

Я считаю, что случай 2 намного легче читать и труднее ошибиться, особенно для коротких фрагментов.

Эдвард КМЕТТ
источник
1

Как бы то ни было, в документации MSDN есть пример возврата изнутри блокировки. Из других ответов здесь, похоже, он очень похож на IL, но мне кажется безопаснее вернуться изнутри блокировки, потому что тогда вы не рискуете перезаписать возвращаемую переменную другим потоком.

серый96
источник
0

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

Адам Ашам
источник
0

lock() return <expression> заявления всегда:

1) введите блокировку

2) делает локальное (поточно-ориентированное) хранилище для значения указанного типа,

3) заполняет магазин значением, возвращаемым <expression> ,

4) блокировка выхода

5) вернуть в магазин.

Это означает, что значение, возвращаемое оператором блокировки, всегда «готовится» перед возвратом.

Не переживайте lock() return, здесь никого не слушайте))

мшакуров
источник
-2

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

Подведем итог и дополним существующие ответы:

  • В ответ принятые показывает , что, независимо от того, какой синтаксис формы вы выбираете в вашей C # код, в код IL - и , следовательно , во время выполнения - returnне произойдет до тех пор , после того, как блокировка будет снята.

    • Несмотря на то, помещая return внутриlock блок поэтому, строго говоря, искажает поток управления [1] , то синтаксический удобно тем , что отпадает необходимость в хранении возвращаемого значения в AUX. локальная переменная (объявленная вне блока, чтобы ее можно было использовать с returnвне блока) - см . ответ Эдварда КМЕТТА .
  • Отдельно - и этот аспект является второстепенным для вопроса, но все же может представлять интерес ( ответ Рикардо Вильямиля пытается решить его, но, как мне кажется, неверно) - объединение lockоператора с returnоператором, то есть получение значения returnв блоке, защищенном от одновременный доступ - значимо «защищает» возвращаемое значение в области действия вызывающего , только если оно действительно не нуждается в защите после получения , что применяется в следующих сценариях:

    • Если возвращаемое значение является элементом из коллекции, который нуждается в защите только с точки зрения добавления и удаления элементов, а не с точки зрения модификации самих элементов и / или ...

    • ... если возвращаемое значение является экземпляром типа значения или строкой .

      • Обратите внимание, что в этом случае вызывающая сторона получает снимок (копию) [2] значения, которое к тому времени, когда вызывающая сторона проверяет, может уже не быть текущим значением в исходной структуре данных.
    • В любом другом случае блокировка должна выполняться вызывающим , а не (только) внутри метода.


[1] Theodor Zoulias указывает на то , что это технически верно и для размещения returnвнутри try, catch, using, if, while, for, ... заявления; однако именно конкретная цель lockутверждения, вероятно, потребует тщательного изучения истинного потока управления, о чем свидетельствует вопрос, который был задан и получил много внимания.

[2] Доступ к экземпляру типа значения неизменно создает его локальную для потока копию в стеке; даже несмотря на то, что строки технически являются экземплярами ссылочного типа, они эффективно ведут себя экземплярами типа аналогичного значения.

mklement0
источник
Что касается текущего состояния вашего ответа (версия 13), вы все еще размышляете о причинах существования lockоператора return и извлекаете смысл из его размещения. Это обсуждение, не связанное с этим вопросом, ИМХО. Также меня беспокоит использование словосочетания «искажение фактов» . Если возвращения из lockискажает поток управления, то же самое можно сказать и о возвращении из try, catch, using, if, while, for, и любая другая конструкция языка. Это все равно что сказать, что C # пронизан искажениями потока управления. Иисус ...
Теодор Зулиас
«Это все равно, что сказать, что C # пронизан искажениями потока управления» - Что ж, технически это верно, и термин «искажение» является лишь оценочным суждением, если вы решите принять его таким образом. С try, if... лично я не склонен даже думать об этом, но и в контексте lock, в частности, возник вопрос для меня - и , если он не возникал для других тоже, этот вопрос никогда бы не спрашивали , и принятый ответ не пошел бы на изучение истинного поведения.
mklement0