리턴 문이 잠금 내부 또는 외부에 있어야합니까?


142

방금 내 코드의 어느 곳에서 잠금 내부와 외부에 return 문이 있음을 깨달았습니다. 어느 것이 최고입니까?

1)

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

2)

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

}

어느 것을 사용해야합니까?


Reflector를 발사하고 IL을 비교하는 것은 어떻습니까 ;-).
팝 카탈린

6
@Pop : 완료 – IL 용어로는 나아지지 않음-C # 스타일 만 적용
Marc Gravell

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
}

IL 수준에서 그것들은 (이름을 지어 주거나) 동일합니다 (나는 무언가를 배웠습니다 ;-p). 따라서 유일하게 현명한 비교는 지역 코딩 스타일의 (매우 주관적인) 법칙입니다 ... 나는 ReturnInside단순성을 선호 하지만 어느 쪽도 흥분하지 않을 것입니다.


15
나는 (무료 및 우수한) Red Gate의 .NET 반사판 (기존 : Lutz Roeder의 .NET 반사판)을 사용했지만 ILDASM도 그렇게 할 것입니다.
Marc Gravell

1
Reflector의 가장 강력한 측면 중 하나는 IL을 원하는 언어 (C #, VB, Delphi, MC ++, Chrome 등)로 실제로 분해 할 수 있다는 것입니다.
Marc Gravell

3
간단한 예제에서 IL은 동일하게 유지되지만 상수 값만 반환하기 때문일 수 있습니다! 실제 시나리오의 경우 결과가 다를 수 있으며 병렬 스레드가 값을 반환하기 전에 값을 수정하여 서로 문제를 일으킬 수 있다고 생각합니다. 반환 명령문은 잠금 블록 외부에 있습니다. 위험한!
Torbjørn

@ MarcGravell : 나는 똑같은 것을 숙고하면서 게시물을 보았지만 대답을 읽은 후에도 여전히 다음에 대해 확신하지 못합니다. 외부 접근 방식을 사용하면 스레드 안전 논리가 깨질 수있는 상황이 있습니까? 나는 단일 리턴 포인트를 선호하고 스레드 안전성에 대해 좋은 느낌을 느끼지 않기 때문에 이것을 묻습니다. 일리노이가 같더라도 어쨌든 저의 우려는 약점입니다.
Raheel Khan

1
@RaheelKhan 아니오, 없음; 그들은 동일합니다. IL 수준에서는 영역 내부 를 사용할 수 없습니다 . ret.try
Marc Gravell

42

아무런 차이가 없습니다. 둘 다 컴파일러에 의해 동일한 것으로 번역됩니다.

명확히하기 위해 다음 의미를 가진 것으로 효과적으로 변환됩니다.

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

return myData;

1
글쎄, 그것은 시도 / 마지막으로 사실이다-그러나 자물쇠 외부의 반환에는 여전히 최적화 할 수없는 여분의 지역 주민이 필요하며 더 많은 코드가 필요합니다 ...
Marc Gravell

3
try 블록에서 돌아올 수 없습니다. ".leave"op 코드로 끝나야합니다. 따라서 방출 된 CIL은 두 경우 모두 동일해야합니다.
Greg Beech

3
당신은 옳습니다-방금 IL을 보았습니다 (업데이트 된 게시물 참조). 나는 무언가를 배웠다 ;-p
Marc Gravell

2
쿨, 불행히도 내 동적 방법 :-(로드 할 CLR의 쓰레기를 시도 블록에 .ret 연산 코드를 방출하는 노력과 가진 고통스러운 시간에서 배운
그렉 비치를

저 공감할 수 있어요; 나는 상당한 양의 Reflection.Emit을 수행했지만 게으르다. 확실하지 않은 경우 C #으로 대표 코드를 작성한 다음 IL을 살펴보십시오. 그러나 얼마나 빨리 IL 용어로 생각하기 시작하는지 놀랍습니다 (즉, 스택 시퀀싱).
Marc Gravell

28

나는 확실히 반품을 자물쇠 안에 넣을 것이다. 그렇지 않으면 다른 스레드가 잠금을 입력하고 return 문 전에 변수를 수정하여 원래 호출자가 예상과 다른 값을 받도록 할 수 있습니다.


4
이것은 정확합니다. 다른 응답자가 누락 된 것 같습니다. 이들이 작성한 간단한 샘플은 동일한 IL을 생성 할 수 있지만 대부분의 실제 시나리오에는 해당되지 않습니다.
Torbjørn

4
나는

5
이 샘플에서는 스택 변수를 사용하여 반환 값, 즉 잠금 외부의 return 문과 물론 변수 선언을 저장하는 방법에 대해 이야기합니다. 다른 스레드에는 다른 스택이 있어야하므로 아무런 해를 끼칠 수 없습니다. 맞습니까?
기예르모 Ruffino

3
다른 스레드가 반환 호출과 반환 값을 기본 스레드의 변수에 실제로 할당하는 사이의 값을 업데이트 할 수 있기 때문에 이것이 유효한 지점이라고 생각하지 않습니다. 반환되는 값은 변경되거나 현재 실제 값과의 일관성을 보장 할 수 없습니다. 권리?
Uroš Joksimović

이 답변은 잘못되었습니다. 다른 스레드는 로컬 변수를 변경할 수 없습니다. 지역 변수는 스택에 저장되며 각 스레드에는 자체 스택이 있습니다. 스레드 스택의 기본 크기는 1MB 입니다.
Theodor Zoulias

5

때에 따라 다르지,

나는 곡물에 반대 할 것입니다. 나는 일반적으로 자물쇠 안쪽으로 돌아갑니다.

일반적으로 변수 mydata는 지역 변수입니다. 로컬 변수를 초기화하는 동안 선언하는 것을 좋아합니다. 내 자물쇠 밖에서 반환 값을 초기화 할 데이터가 거의 없습니다.

따라서 비교에는 실제로 결함이 있습니다. 이상적으로 두 옵션의 차이점은 당신이 작성한 것과 같을 것입니다. 사례 1에 끄덕이는 것은 실제로 조금 더 추합니다.

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

vs.

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

특히 짧은 스 니펫의 경우 사례 2가 읽기 쉽고 조이기 어렵다는 것을 알았습니다.


4

외부 잠금이 더 좋아 보이지만 코드를 다음과 같이 변경하면 조심하십시오.

return f(...)

잠금을 유지 한 상태에서 f ()를 호출해야하는 경우 일관성을 위해 잠금 내부에 리턴을 유지하는 것이 분명하기 때문에 잠금 내부에 있어야합니다.


1

가치가있는 것을 위해 MSDN문서 에는 자물쇠 내부에서 돌아 오는 예가 있습니다. 여기에있는 다른 답변에서 IL과 매우 유사한 것처럼 보이지만 다른 스레드가 반환 변수를 덮어 쓸 위험이 없으므로 잠금 내부에서 반환하는 것이 더 안전 해 보입니다.


0

동료 개발자가 코드를 쉽게 읽을 수 있도록 첫 번째 대안을 제안합니다.


0

lock() return <expression> 항상 진술 :

1) 자물쇠를 입력

2) 지정된 유형의 값을 로컬 (스레드 안전) 저장소에 저장합니다.

3)에 의해 반환 된 값 저장소를 채우고 <expression>,

4) 출구 잠금

5) 가게를 반환하십시오.

잠금 명령문에서 리턴 된 값은 리턴 전에 항상 "요리"되었음을 의미합니다.

걱정하지 마십시오. lock() return여기에 다른 사람의 말을 듣지 마십시오 ))


-2

참고 : 나는이 답변이 사실 정확하다고 생각하며 도움이되기를 바랍니다. 그러나 구체적인 피드백을 바탕으로 항상 개선 해 드리겠습니다.

기존 답변을 요약하고 보완하려면 :

  • 허용 응답 에 관계없이 어떤 문법 형태의 당신이 당신의 선택, 쇼 C #을 런타임에 따라서 그리고 - - (가) 일리노이 코드, 코드 return때까지 발생하지 않습니다 잠금이 해제됩니다.

    • 심지어 배치하더라도 return 내부lock 부정확하게에게, 제어 흐름 엄밀히 말하면 블록 따라서 [1] , 이는 문법적이고 편리한 그것이 AUX에 반환 값을 저장할 필요성을 제거하는 것이있다. 지역 변수 (블록 외부와 함께 사용할 수 있도록 블록 외부에 선언 return) -Edward KMETT의 답변을 참조하십시오 .
  • 별도로 - 그리고 이러한 측면은 부수적 질문에,하지만 여전히 (관심을 가질 수있는 리카르도 Villamil의 대답의 시도가 그것을 해결하기 위해,하지만 잘못, 내 생각) - 합성 lock로 문을 return문 - 즉, 가치를 획득 return로부터 보호 블록에 동시 액세스- 일단 획득 한 후 실제로 보호 할 필요가없는 경우 호출자 의 범위 에서 반환 된 값을 의미있게 "보호"하는 것은 다음 시나리오에 적용됩니다.

    • 반환 값이 컬렉션 의 요소 인 경우 요소 자체 의 수정 및 / 또는 요소가 아닌 요소 추가 및 제거 측면에서만 보호가 필요한 경우 ...

    • ... 값은 인스턴스 인 경우에 반환되는 값 유형 또는 스트링 .

      • 이 경우 호출자는 값의 스냅 샷 (복사) [2] 을 수신합니다. 호출자는이를 검사 할 때 더 이상 원래 데이터 구조의 현재 값이 아닐 수 있습니다.
    • 다른 경우 에는 메서드 내부가 아닌 호출자 가 잠금을 수행해야합니다 .


[1] 테오도르 Zoulias은 그 기술적 또한 사실 배치하는 것이 지적 return내부 try, catch, using, if, while, for, ... 문; 그러나, lock이 질문이 요청되어 많은 관심을 받았음에 따라, 진정한 통제 흐름에 대한 조사를 요구할 가능성 이있는 진술 의 구체적인 목적이다 .

[2] 값 유형 인스턴스에 액세스하면 스레드 로컬의 스택 사본이 생성됩니다. 문자열은 기술적으로 참조 형 인스턴스이지만 값과 유사한 유형의 인스턴스처럼 효과적으로 동작합니다.


답의 현재 상태 (개정판 13)와 관련하여 여전히의 존재 이유에 대해 추측하고 lock있으며 return 문을 배치하여 의미를 도출하고 있습니다. 이 질문 IMHO와 관련이없는 토론은 어느 것입니까? 또한 나는 사용법을 발견 "misrepresents" 상당히 혼란 스럽습니다. A로부터 복귀하면 lock부정확하게 제어의 흐름을 한 후 동일한가 돌아가는 상기 수 try, catch, using, if, while, for, 및 임의의 다른 언어 구조. C #에 제어 흐름 오류 표시가 수수께끼라는 말과 같습니다. 예수 ...
Theodor Zoulias

"C #이 제어 흐름 허위 표시로 수수께끼를 말하는 것과 같습니다."-기술적으로 사실이며, "허위 표시"라는 용어는 그러한 방식으로 선택하면 가치 판단 일뿐입니다. 함께 try, if... 나는 개인적으로 그것에 대해 생각하는 경향이 없지만,의 맥락에서 lock특히, 문제는 나를 위해 발생 -와 너무 다른 사람을 위해 발생하지 않은 경우,이 질문은 질문하지 않았을 것이다 승인 된 답변은 실제 행동을 조사하기 위해 많은 시간이 걸리지 않았습니다.
mklement0
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.