Должен ли оператор return быть внутри или снаружи блокировки? - PullRequest
126 голосов
/ 06 ноября 2008

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

1)

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

2)

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

}

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

Ответы [ 9 ]

170 голосов
/ 06 ноября 2008

По сути, что делает код проще. Одиночная точка выхода - хороший идеал, но я бы не стал изгибать код просто для его достижения ... И если альтернатива объявляет локальную переменную (вне блокировки), инициализирует ее (внутри блокировки) и затем возвращая его (вне блокировки), я бы сказал, что простой "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 для простоты, но я бы тоже не обрадовался.

37 голосов
/ 06 ноября 2008

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

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

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

return myData;
30 голосов
/ 07 ноября 2008

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

5 голосов
/ 06 ноября 2008

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

return f(...)

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

4 голосов
/ 06 ноября 2008

Зависит от

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

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

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

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

против

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

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

1 голос
/ 24 июня 2013

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

1 голос
/ 06 ноября 2008

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

0 голосов
/ 27 декабря 2017

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

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

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

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

4) выходной замок

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

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

Не беспокойся о lock() return, не слушай никого здесь))

0 голосов
/ 06 ноября 2008

Снаружи выглядит чище.

...