이 코드는 릴리스 모드에서 중단되지만 디버그 모드에서는 제대로 작동합니다.


110

나는 이것을 발견하고 디버그 및 릴리스 모드 에서이 동작의 이유를 알고 싶습니다.

public static void Main(string[] args)
{            
   bool isComplete = false;

   var t = new Thread(() =>
   {
       int i = 0;

        while (!isComplete) i += 0;
   });

   t.Start();

   Thread.Sleep(500);
   isComplete = true;
   t.Join();
   Console.WriteLine("complete!");
}

25
행동의 차이점은 정확히 무엇입니까?
Mong Zhu

4
자바라면 컴파일러가 'compile'변수에 대한 업데이트를 보지 않는다고 가정합니다. 변수 선언에 'volatile'을 추가하면 문제가 해결되고 정적 필드가됩니다.
Sebastian


4
참고 : 이것이 다중 스레드 개발에 뮤텍스 및 원자 연산과 같은 것이있는 이유입니다. 멀티 스레딩을 시작하면 이전에는 분명하지 않았던 놀라운 추가 메모리 문제를 고려해야합니다. 뮤텍스와 같은 스레딩 동기화 도구는이 문제를 해결했을 것입니다.
Cort Ammon

5
@DavidSchwartz : 물론 허용됩니다 . 그리고 컴파일러, 런타임 및 CPU가 해당 코드의 결과를 예상 한 것과 다르게 만드는 것이 허용됩니다 . 특히 C #은 bool 액세스를 nonatomic 으로 만드는 것이 허용되지 않지만 비 휘발성 읽기를 적시에 뒤로 이동할 수 있습니다. 대조적으로 복식은 원자성에 대한 제한이 없습니다. 동기화없이 두 개의 서로 다른 스레드에서 이중 읽기 및 쓰기는 찢어 질 수 있습니다.
Eric Lippert

답변:


149

나는 옵티마이 저가 isComplete변수 에 'volatile'키워드가 없다는 것에 속았다 고 생각 합니다.

물론 로컬 변수이기 때문에 추가 할 수 없습니다. 그리고 물론, 그것은 지역 변수이기 때문에 전혀 필요하지 않아야합니다. 지역 변수는 스택에 유지되고 자연스럽게 항상 "신선"하기 때문입니다.

그러나 컴파일 후에는 더 이상 로컬 변수아닙니다 . 익명 대리자에서 액세스되기 때문에 코드가 분할되고 다음과 같은 도우미 클래스 및 멤버 필드로 변환됩니다.

public static void Main(string[] args)
{
    TheHelper hlp = new TheHelper();

    var t = new Thread(hlp.Body);

    t.Start();

    Thread.Sleep(500);
    hlp.isComplete = true;
    t.Join();
    Console.WriteLine("complete!");
}

private class TheHelper
{
    public bool isComplete = false;

    public void Body()
    {
        int i = 0;

        while (!isComplete) i += 0;
    }
}

이제 다중 스레드 환경의 JIT 컴파일러 / 최적화 프로그램이 TheHelper클래스를 처리 할 때 메서드 시작시 일부 레지스터 또는 스택 프레임 의 값 을 실제로 캐시 하고 메서드가 끝날 때까지 새로 고칠 수 없다고 상상할 수 있습니다 . 스레드 및 메서드가 "= true"가 실행되기 전에 끝나지 않을 것이라는 보장이 없기 때문입니다. 따라서 보장이 없다면 캐시하지 않고 힙 개체를 매번 읽는 대신 한 번 읽는 성능 향상을 얻으십시오. 되풀이.falseBody()

이것이 바로 키워드 volatile가 존재 하는 이유 입니다.

이 헬퍼 클래스가 조금 더 정확 해지 려면 1) 다중 스레드 환경에서 다음이 있어야합니다.

    public volatile bool isComplete = false;

하지만 물론 자동 생성 된 코드이므로 추가 할 수 없습니다. 더 나은 접근 방식은 lock()읽기 및 쓰기에 대한 s 를 추가 isCompleted하거나 베어 메탈을 시도하는 대신 다른 즉시 사용 가능한 동기화 또는 스레딩 / 태스킹 유틸리티를 사용하는 것 입니다 . GC, JIT 및 (..))이있는 CLR의 C #이기 때문입니다.

디버그 모드의 차이는 아마도 디버그 모드에서 많은 최적화가 제외 되었기 때문에 발생하므로 화면에 표시된 코드를 디버그 할 수 있습니다. 따라서 while (!isComplete)중단 점을 설정할 수 있도록 최적화되지 않았으므로 isComplete메서드 시작시 레지스터 또는 스택에 적극적으로 캐시되지 않으며 모든 루프 반복에서 힙의 개체에서 읽습니다.

BTW. 그건 내 추측 일뿐입니다. 나는 그것을 컴파일하려고 시도조차하지 않았다.

BTW. 버그가 아닌 것 같습니다. 매우 모호한 부작용에 가깝습니다. 또한 내가 맞다면 언어 부족 일 수 있습니다. C #은 캡쳐되고 클로저의 멤버 필드로 승격되는 지역 변수에 '휘발성'키워드를 배치 할 수 있어야합니다.

1) 아래에서 Eric Lippert의 의견 volatile및 / 또는 코드에 의존하는 코드 volatile안전한지 확인하는 데 관련된 복잡성 수준을 보여주는 매우 흥미로운 기사를 참조하십시오. ..uh, good ..uh, OK라고합시다.


2
@EricLippert : 워, 그렇게 빨리 확인 해주셔서 감사합니다! 어떻게 생각 volatile하십니까? 향후 버전 에서 캡쳐-클로저 지역 변수에 대한 옵션을 얻을 수있는 가능성이 있습니까? 나는 ..이 컴파일러에 의해 처리하기 어려운 조금 할 수 있습니다 상상
케찰코아틀

7
@quetzalcoatl :이 기능이 곧 추가 될 것이라고 생각하지 않습니다. 이것은 당신이 권장하지 않는 코딩의 종류이며 더 쉽게 만들지 않습니다 . 게다가, 물건을 휘발성으로 만드는 것이 반드시 모든 문제를 해결하는 것은 아닙니다. 다음은 모든 것이 휘발성이고 프로그램이 여전히 잘못된 예입니다. 버그를 찾을 수 있습니까? blog.coverity.com/2014/03/26/reordering-optimizations
Eric Lippert

3
알겠습니다. 멀티 스레딩 최적화를 이해하려는 시도를 포기합니다 ... 얼마나 복잡한 지 제정신이 아닙니다.
InBetween

10
@Pikoh : 다시 한 번, 옵티 마이저처럼 생각하십시오. 증분되지만 읽지 않는 변수가 있습니다. 읽지 않는 변수는 완전히 삭제할 수 있습니다.
Eric Lippert

4
@EricLippert 이제 내 마음이 클릭을했습니다. 이 스레드는 매우 유익했습니다. 정말 감사합니다.
Pikoh

82

케찰코아틀대답 이 맞습니다. 더 많은 빛을 비추려면 :

C # 컴파일러 및 CLR 지터는 현재 스레드가 실행중인 유일한 스레드라고 가정하는 많은 최적화를 수행 할 수 있습니다. 이러한 최적화로 인해 현재 스레드가 실행중인 유일한 스레드가 아닌 세계에서 프로그램이 잘못된 경우 문제가 됩니다. 당신은있다 필요한 당신이하고있는 멀티 스레드 미친 어떤 물건 컴파일러와 지터에게 멀티 스레드 프로그램을 작성 할 수 있습니다.

이 특별한 경우에 지터는 루프 본체에 의해 변수가 변경되지 않았 음을 관찰하고 따라서 이것이 유일한 스레드 실행이라는 가정하에 변수가 변경되지 않는다는 결론을 내리기 위해 허용되지만 필수는 아닙니다. 변경되지 않으면 변수가 진실인지 확인해야합니다. 는 루프를 통해 매번이 아니라 한 번 . 그리고 이것은 실제로 일어나고있는 것입니다.

이것을 해결하는 방법? 다중 스레드 프로그램을 작성하지 마십시오 . 멀티 스레딩은 전문가에게조차도 제대로하기가 매우 어렵습니다. 필요한 경우 가장 높은 수준의 메커니즘을 사용하여 목표를 달성하십시오 . 여기서 해결책은 변수를 휘발성으로 만드는 것이 아닙니다. 여기서 해결 방법은 취소 가능한 작업작성하고 작업 병렬 라이브러리 취소 메커니즘을 사용하는 것 입니다. TPL이 스레딩 논리를 올바르게 가져오고 취소가 스레드간에 제대로 전송되는 것에 대해 걱정하게하십시오.


1
의견은 확장 된 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
Madara 's Ghost

14

나는 실행중인 프로세스에 연결하고 Thread방법이 다음과 같이 번역 된다는 것을 발견했습니다 (실수하지 않았다면 이것으로별로 연습하지 않습니다) .

debug051:02DE04EB loc_2DE04EB:                            
debug051:02DE04EB test    eax, eax
debug051:02DE04ED jz      short loc_2DE04EB
debug051:02DE04EF pop     ebp
debug051:02DE04F0 retn

eax(값 포함 isComplete)은 처음로드되고 새로 고쳐지지 않습니다.


8

실제로 답은 아니지만 문제에 대해 좀 더 밝힐 수 있습니다.

문제는 때 것으로 보인다 i람다 본체 내부에 선언 하고 이 있어요 에만 할당 식 읽어 보시기 바랍니다. 그렇지 않으면 코드가 릴리스 모드에서 잘 작동합니다.

  1. i 람다 본문 외부에서 선언 :

    int i = 0; // Declared outside the lambda body
    
    var t = new Thread(() =>
    {
        while (!isComplete) { i += 0; }
    }); // Completes in release mode
  2. i 할당 표현식에서 읽지 않습니다.

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { i = 0; }
    }); // Completes in release mode
  3. i 다른 곳에서도 읽습니다.

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { Console.WriteLine(i); i += 0; }
    }); // Completes in release mode

내 내기는 일부 컴파일러 또는 JIT 최적화입니다. i 가 일을 엉망으로 만드는 것입니다. 나보다 똑똑한 사람이이 문제에 대해 더 많은 것을 밝힐 수있을 것입니다.

그럼에도 불구하고 비슷한 코드가 실제로 어떤 용도로 사용 될지 알 수 없기 때문에 나는 그것에 대해 너무 걱정하지 않을 것입니다.


1
나는 확신, 내 대답을 참조 그거야에 대한 모든 '휘발성'키워드 thatcannot (실제로 이후 폐쇄의 멤버 필드로 승진 도착하는) .. 지역 변수에 추가
케찰코아틀
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.