Якщо виписка про повернення повинна бути всередині або поза замком?


142

Я щойно зрозумів, що я десь у своєму коді маю виписку повернення всередині блокування, а колись і зовні. Який із них найкращий?

1)

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

2)

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

}

Який я повинен використовувати?


Як щодо стрільби Reflector і зробити порівняння ІЛ ;-).
Поп Каталін

6
@Pop: зроблено - ні в кращих випадках, ні в ІЛ - не застосовується тільки стиль C #
Марк Гравелл

1
Дуже цікаво, о, я сьогодні чогось навчусь!
Pokus

Відповіді:


192

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

Тож на рівні ІЛ вони [дають або беруть якісь імена] однакові (я щось навчився ;-p). Таким чином, єдине розумне порівняння - це (дуже суб'єктивний) закон локального стилю кодування ... Я віддаю перевагу ReturnInsideпростоті, але я ні про що не хвилююся.


15
Я використовував (вільний і відмінний) .NET Reflector Red Gate (був: .NET Reflector Лутза Родера), але і ILDASM теж зробив би це.
Марк Гравелл

1
Один з найпотужніших аспектів Reflector - це те, що ви можете фактично розібрати IL на бажану мову (C #, VB, Delphi, MC ++, Chrome тощо)
Marc Gravell

3
Для вашого простого прикладу ІЛ залишається таким же, але це, мабуть, тому, що ви повертаєте лише постійне значення ?! Я вважаю, що в сценаріях реального життя результат може відрізнятися, а паралельні потоки можуть створювати проблеми один для одного, змінюючи значення до того, як воно повернеться, оператор return повертається поза блоком блокування. Небезпечно!
Torbjørn

@MarcGravell: Я щойно натрапив на ваш пост, розмірковуючи над тим самим, і навіть прочитавши вашу відповідь, я все ще не впевнений у наступному: Чи існують ЯКІ-небудь обставини, коли використання зовнішнього підходу може порушити логіку безпеки. Я запитую це, оскільки я віддаю перевагу одній точці повернення і не "ВІДПОВІДУЮ" добре про її безпеку ниток. Хоча, якщо ІЛ однакова, то в будь-якому разі моє занепокоєння має бути суперечливим.
Рахіль Хан

1
@RaheelKhan ні, немає; вони однакові. На рівні ІЛ ви не можете ret всередині .tryрегіону.
Марк Гравелл

42

Це не має ніякої різниці; вони обоє перекладені компілятором на одне й те саме.

Для уточнення, або ефективно перекладається на щось із наступною семантикою:

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

return myData;

1
Ну, це правда щодо спроби / нарешті - однак, повернення поза замком все ще вимагає додаткових місцевих жителів, яких неможливо оптимізувати - і забирає більше коду ...
Марк Гравелл

3
Ви не можете повернутися з пробного блоку; він повинен закінчуватися оп-кодом ".leave". Таким чином, викид CIL повинен бути однаковим в будь-якому випадку.
Грег Бук

3
Ви маєте рацію - я щойно переглянув ІЛ (див. Оновлений пост). Я щось дізнався
;-p

2
Класно, на жаль, я дізнався з болісних годин, намагаючись випромінювати .ret-up-коди у пробних блоках, і CLR відмовився завантажувати свої динамічні методи :-(
Грег Бук

Я можу співвідносити; Я зробив неабияку кількість Reflection.Emit, але я лінивий; якщо я в чомусь не впевнений, я пишу представницький код у C # і потім дивлюсь на ІЛ. Але дивно, як швидко ти починаєш думати в ІЛ термінах (тобто послідовності складання).
Марк Гравелл

28

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


4
Це правильно, пункт, який, здається, інших респондентів не вистачає. Прості зразки, які вони зробили, можуть давати той же ІЛ, але це не так для більшості реальних сценаріїв.
Torbjørn

4
Я здивований, що інші відповіді не говорять про це
Акшат Агарвал

5
У цьому зразку вони говорять про використання змінної стека для зберігання повернутого значення, тобто лише оператора return за межами блокування та, звичайно, декларації змінної. Інша нитка повинна мати ще один стек, і таким чином не могла б заподіяти шкоди, я прав?
Гільєрмо Руффіно

3
Я не думаю, що це правильна точка, оскільки інший потік міг би оновити значення між зворотним викликом і фактичним присвоєнням значення повернення змінній основного потоку. Повернене значення не може бути змінене або гарантоване відповідність поточному фактичному значенню. Правильно?
Урош Йоксимович

Ця відповідь невірна. Інший потік не може змінити локальну змінну. Локальні змінні зберігаються в стеку, і кожен потік має свій стек. Btw за замовчуванням розмір стека нитки - 1 Мб .
Теодор Зуліяс

5

Це залежить,

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

Зазвичай мідані змінної є локальною змінною. Я захоплююсь декларуванням локальних змінних під час їх ініціалізації. У мене рідко є дані, щоб ініціалізувати моє повернене значення поза моїм замком.

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

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

vs.

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

Мені здається, що випадок 2 значно легше читати, і важче викручувати, особливо для коротких фрагментів.


4

Якщо ви думаєте, що замок зовні виглядає краще, але будьте обережні, якщо ви врешті-решт змінили код на:

return f(...)

Якщо f () потрібно викликати замок, який утримується, він, очевидно, повинен бути всередині блокування, оскільки таке збереження повертається всередині блоку для послідовності має сенс.


1

Для чого це варто, документація на MSDN містить приклад повернення зсередини замка. З інших відповідей тут, здається, це дуже схоже ІЛ, але, мені здається, безпечніше повернутися зсередини блокування, оскільки тоді ви не ризикуєте, що змінна повернення буде перезаписана іншим потоком.


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] Доступ до екземпляра типу значення незмінно створює локальну потокову копію його на стеці; незважаючи на те, що рядки є технічно референсними екземплярами, вони ефективно поводяться як екземпляри типу значень.


Що стосується поточного стану вашої відповіді (редакція 13), ви все ще спекулюєте над причинами існування lockта випливає значення із розміщення зворотного твердження. Яка дискусія не пов'язана з цим питанням ІМХО. Крім того, я вважаю, що використання "неправильних репрезентацій" є дуже тривожним. Якщо повернення з lockспотворює потік управління, те ж саме можна сказати і про повернення з try, catch, using, if, while, for, і будь-яка інша конструкція мови. Це як би сказати, що C # пронизаний неправильними поданнями потоку управління. Ісусе ...
Теодор Зуліяс

"Це як би сказати, що C # пронизаний хибними поданнями контрольних потоків" - Ну, це технічно правда, і термін "хибна презентація" є лише оціночним судженням, якщо ви вирішите взяти це таким чином. З try, if... особисто я не схильний навіть думати про це, але і в контексті lock, в зокрема, виникло питання для мене - і , якщо воно не виникало для інших теж, це питання ніколи б не питали і прийнята відповідь не пішла б з великими зусиллями, щоб дослідити справжню поведінку.
mklement0
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.