문제
다음 두 개의 스 니펫에서 첫 번째 스 니펫이 안전합니까, 아니면 두 번째 스 니펫을해야합니까?
안전하다는 것은 각 스레드가 스레드가 생성 된 것과 동일한 루프 반복에서 FOO의 메소드를 호출하도록 보장된다는 것을 의미합니까?
아니면 루프의 각 반복에 새로운 변수 "로컬"에 대한 참조를 복사해야합니까?
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의 답변에서 지적했듯이, 이것은 스레딩과 특별히 관련이 없습니다.
해결책
편집 :이 모든 것은 C# 5의 변경으로 변수가 정의되는 위치 (컴파일러의 눈에)가 변경됩니다. 에서 C# 5 이후에는 동일합니다.
C#5 이전
두 번째는 안전합니다. 첫 번째는 아닙니다.
와 함께 foreach
, 변수가 선언됩니다 밖의 루프 - 즉
Foo f;
while(iterator.MoveNext())
{
f = iterator.Current;
// do something with f
}
이것은 단지 1 만 있음을 의미합니다 f
클로저 범위와 관련하여 스레드는 혼란 스러울 수 있습니다. 일부 인스턴스에서 여러 번 메소드를 호출하지는 않습니다. 두 번째 변수 선언 으로이 문제를 해결할 수 있습니다 내부에 루프 :
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();
}
(각 숫자는 한 번이지만 물론 주문은 보장되지 않습니다)
다른 팁
팝 카탈린과 마크 그라벨의 대답이 맞습니다. 내가 추가하고 싶은 것은 링크입니다 폐쇄에 관한 내 기사 (Java와 C#에 대해 이야기합니다). 약간의 가치를 더할 수 있다고 생각했습니다.
편집 : 스레딩의 예측 불가능 성이없는 모범을 보일 가치가 있다고 생각합니다. 다음은 두 가지 접근 방식을 모두 보여주는 짧지 만 완전한 프로그램입니다. "나쁜 행동"목록은 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();
}
}
}
옵션 2를 사용해야 할 필요가 있으므로 변경 변수를 중심으로 클로저를 생성하면 변수가 사용되고 폐쇄 생성 시간이 아닌 변수의 값이 사용됩니다.
편집 : 명확하게하기 위해 C# 클로저에서는 "어휘 폐쇄"변수의 값을 캡처하지 않고 변수 자체를 캡처한다는 의미입니다. 즉, 변경 변수로의 폐쇄를 만들 때 폐쇄는 실제로 값의 사본이 아닌 변수에 대한 참조입니다.
EDIT2 : 컴파일러 내부에 대해 읽는 데 관심이있는 사람이라면 모든 블로그 게시물에 링크가 추가되었습니다.
이것은 흥미로운 질문이며 사람들이 다양한 방식으로 대답하는 것을 본 것 같습니다. 나는 두 번째 방법이 유일한 안전한 방법이 될 것이라는 인상을 받았습니다. 나는 진짜 빠른 증거를 휘젓었다 :
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이 확실히 안전하지 않습니다.
귀하의 경우, 귀하는 매핑하여 복사 트릭을 사용하지 않고 문제를 피할 수 있습니다. ListOfFoo
스레드 시퀀스로 :
var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething()));
foreach (var t in threads)
{
t.Start();
}
둘 다 C# 버전 5 (.NET Framework 4.5) 기준으로 안전합니다. 자세한 내용은이 질문을 참조하십시오. Foreach의 변수 사용이 C# 5에서 변경 되었습니까?
Foo f2 = f;
동일한 참조를 가리 킵니다
f
그래서 잃어버린 것은없고 얻지 못했습니다 ...