foreach 식별자 및 클로저


79

다음 두 스 니펫에서 첫 번째는 안전한가요, 아니면 두 번째는해야합니까?

안전하다는 것은 각 스레드가 스레드가 생성 된 동일한 루프 반복에서 Foo의 메서드를 호출하도록 보장된다는 것을 의미합니까?

아니면 새로운 변수 "local"에 대한 참조를 루프의 각 반복에 복사해야합니까?

var threads = new List<Thread>();
foreach (Foo f in ListOfFoo)
{      
    Thread thread = new Thread(() => f.DoSomething());
    threads.Add(thread);
    thread.Start();
}

-

var threads = new List<Thread>();
foreach (Foo f in ListOfFoo)
{      
    Foo f2 = f;
    Thread thread = new Thread(() => f2.DoSomething());
    threads.Add(thread);
    thread.Start();
}

업데이트 : Jon Skeet의 답변에서 지적했듯이 이것은 스레딩과 특별히 관련이 없습니다.


실제로 스레딩을 사용하지 않는 것처럼 스레딩과 관련이 있다고 생각합니다. 올바른 대리자를 호출합니다. 스레딩이없는 Jon Skeet의 샘플에서 문제는 두 개의 루프가 있다는 것입니다. 여기에 하나만 있으므로 코드가 언제 실행 될지 정확히 알지 못하는 한 문제가 없어야합니다 (스레딩을 사용하는 경우-Marc Gravell의 답변에 완벽하게 표시됨).
user276648 dec.


@ user276648 스레딩이 필요하지 않습니다. 루프가 끝날 때까지 델리게이트 실행을 연기하면이 동작을 충분히 얻을 수 있습니다.
binki

답변:


103

편집 :이 모든 변경 사항은 C # 5에서 변수가 정의 된 위치 (컴파일러의 관점에서)로 변경되었습니다. 에서 C # 5 이후, 그들은 동일합니다 .


C # 5 이전

두 번째는 안전합니다. 첫 번째는 그렇지 않습니다.

를 사용 foreach하면 변수가 루프 외부 에서 선언 됩니다 .

Foo f;
while(iterator.MoveNext())
{
     f = iterator.Current;
    // do something with f
}

f, 클로저 범위가 1 개 뿐이며 스레드가 혼동 될 가능성이 매우 높습니다. 일부 인스턴스에서는 메서드를 여러 번 호출하고 다른 인스턴스에서는 전혀 호출하지 않습니다. 루프 내부 의 두 번째 변수 선언으로이 문제를 해결할 수 있습니다 .

foreach(Foo f in ...) {
    Foo tmp = f;
    // do something with tmp
}

그러면 tmp각 클로저 범위에 별도 의 항목이 있으므로이 문제의 위험이 없습니다.

다음은 문제에 대한 간단한 증거입니다.

    static void Main()
    {
        int[] data = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        foreach (int i in data)
        {
            new Thread(() => Console.WriteLine(i)).Start();
        }
        Console.ReadLine();
    }

출력 (무작위) :

1
3
4
4
5
7
7
8
9
9

임시 변수를 추가하면 작동합니다.

        foreach (int i in data)
        {
            int j = i;
            new Thread(() => Console.WriteLine(j)).Start();
        }

(각 번호는 한 번이지만 주문은 보장되지 않습니다)


이런 소 ... 그 오래된 게시물이 나에게 많은 두통을 덜어주었습니다. 항상 foreach 변수가 루프 내에서 범위가 지정 될 것으로 예상했습니다. 그것은 하나의 중요한 WTF 경험이었습니다.
크리스

사실 그것은 foreach-loop의 버그로 간주되었고 컴파일러에서 수정되었습니다. (달리 용 루프 변수가 전체 루프의 단일 인스턴스를 갖는다.)
Ihar가 묻어

@Orlangur 저는 몇 년 동안 Eric, Mads, Anders와 직접 대화를 나눴습니다. 컴파일러는 사양을 따랐으므로 옳았습니다. 사양이 선택되었습니다. 간단히 : 그 선택이 변경되었습니다.
Marc Gravell

이 대답은 C # 4까지 적용되지만 이후 버전에는 적용되지 않습니다. "C # 5에서는 foreach의 루프 변수가 논리적으로 루프 내부에 있으므로 매번 변수의 새 복사본에 대해 클로저가 닫힙니다." ( Eric Lippert )
Douglas

@Douglas aye, 우리가 진행하면서이 문제를 수정했지만 일반적인 걸림돌이 되었기 때문에 가야 할 것이 꽤 많습니다!
Marc Gravell

37

Pop Catalin과 Marc Gravell의 답변이 맞습니다. 추가하고 싶은 것은 클로저에 대한 기사 링크입니다 (Java와 C # 모두에 대해 이야기 함). 약간의 가치를 더할 수 있다고 생각했습니다.

편집 : 스레딩의 예측 불가능 성이없는 예제를 제공하는 것이 가치가 있다고 생각합니다. 다음은 두 가지 접근 방식을 보여주는 짧지 만 완전한 프로그램입니다. "bad action"목록은 10 번 10 번 인쇄됩니다. "좋은 조치"목록은 0에서 9까지입니다.

using System;
using System.Collections.Generic;

class Test
{
    static void Main() 
    {
        List<Action> badActions = new List<Action>();
        List<Action> goodActions = new List<Action>();
        for (int i=0; i < 10; i++)
        {
            int copy = i;
            badActions.Add(() => Console.WriteLine(i));
            goodActions.Add(() => Console.WriteLine(copy));
        }
        Console.WriteLine("Bad actions:");
        foreach (Action action in badActions)
        {
            action();
        }
        Console.WriteLine("Good actions:");
        foreach (Action action in goodActions)
        {
            action();
        }
    }
}

1
감사합니다-스레드에 관한 것이 아니라는 질문을 추가했습니다.
xyz

그것은 또한 당신이 당신의 사이트 csharpindepth.com/Talks.aspx의
missaghi

예, 제가 그곳에서 스레딩 버전을 사용했던 것을 기억하는 것 같습니다. 피드백 제안 중 하나는 스레드를 피하는 것이 었습니다. 위와 같은 예제를 사용하는 것이 더 명확합니다.
Jon Skeet

비디오를 알 니스 지켜지고 있지만 :)
존 소총

변수가 for루프 외부에 존재한다는 사실을 이해 하더라도이 동작은 나에게 혼란 스럽습니다. 예를 들어, 클로저 동작의 예인 stackoverflow.com/a/428624/20774 에서 변수는 클로저 외부에 있지만 올바르게 바인딩됩니다. 왜 다른가요?
James McMahon 2016

17

옵션 2를 사용해야합니다. 변경되는 변수 주변에 클로저를 생성하면 클로저 생성 시가 아니라 변수가 사용될 때 변수 값이 사용됩니다.

C #의 익명 메서드 구현 및 그 결과 (1 부)

C #의 익명 메서드 구현 및 그 결과 (2 부)

C #의 익명 메서드 구현 및 그 결과 (3 부)

편집 : 명확하게하기 위해 C # 클로저에서 " 어휘 클로저 "는 변수의 값이 아니라 변수 자체를 캡처한다는 의미입니다. 즉, 변경되는 변수에 대한 클로저를 만들 때 클로저는 실제로 값의 복사본이 아니라 변수에 대한 참조입니다.

Edit2 : 컴파일러 내부에 대해 읽고 싶은 사람이 있으면 모든 블로그 게시물에 대한 링크를 추가했습니다.


나는 그것이 가치와 참조 유형에 해당한다고 생각합니다.
leppie

3

이것은 흥미로운 질문이며 사람들이 다양한 방식으로 대답하는 것을 본 것 같습니다. 두 번째 방법이 유일한 안전한 방법이라는 인상을 받았습니다. 나는 진짜 빠른 증거를 채찍질했다.

class Foo
{
    private int _id;
    public Foo(int id)
    {
        _id = id;
    }
    public void DoSomething()
    {
        Console.WriteLine(string.Format("Thread: {0} Id: {1}", Thread.CurrentThread.ManagedThreadId, this._id));
    }
}
class Program
{
    static void Main(string[] args)
    {
        var ListOfFoo = new List<Foo>();
        ListOfFoo.Add(new Foo(1));
        ListOfFoo.Add(new Foo(2));
        ListOfFoo.Add(new Foo(3));
        ListOfFoo.Add(new Foo(4));


        var threads = new List<Thread>();
        foreach (Foo f in ListOfFoo)
        {
            Thread thread = new Thread(() => f.DoSomething());
            threads.Add(thread);
            thread.Start();
        }
    }
}

이것을 실행하면 옵션 1이 확실히 안전하지 않다는 것을 알 수 있습니다.


1

귀하의 경우 ListOfFoo에는 스레드 시퀀스 에 매핑하여 복사 트릭을 사용하지 않고도 문제를 피할 수 있습니다 .

var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething()));
foreach (var t in threads)
{
    t.Start();
}


-5
Foo f2 = f;

다음과 같은 참조를 가리킴

f 

따라서 잃어버린 것도없고 얻은 것도 없습니다 ...


1
마법이 아닙니다. 단순히 환경을 포착합니다. 여기와 for 루프의 문제는 캡처 변수가 변경 (재 할당)된다는 것입니다.
leppie

1
leppie : 컴파일러는 당신을 위해 코드를 생성하고, 이것이 정확히 어떤 코드 인지 일반적으로 쉽게 알 수 없습니다 . 이것이 컴파일러 마법 정의입니다.
Konrad Rudolph

3
@leppie : 저는 여기 Konrad와 함께 있습니다. 컴파일러가 마법처럼 느껴지는 길이는 의미가 명확하게 정의되어 있지만 잘 이해되지는 않습니다. 마술에 필적하는 잘 이해되지 않는 것에 대한 오래된 격언은 무엇입니까?
Jon Skeet

2
@Jon Skeet "충분히 발전된 기술은 마법과 구별 할 수 없다"는 뜻
입니까?

2
참조를 가리 키지 않습니다. 참고입니다. 동일한 객체를 가리 키지 만 다른 참조입니다.
mqp
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.