質問

次の 2 つのスニペットでは、最初のスニペットは安全ですか、それとも 2 番目のスニペットを実行する必要がありますか?

安全とは、各スレッドがそのスレッドが作成されたのと同じループ反復から 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以前

2 番目は安全です。1つ目はそうではありません。

foreach, 、変数が宣言されています ループ - つまり

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

つまり、1つしかないということです f クロージャーのスコープに関しては、スレッドが混乱する可能性が非常に高くなります。つまり、一部のインスタンスではメソッドが複数回呼び出され、他のインスタンスではまったく呼び出されない可能性があります。これは 2 番目の変数宣言で修正できます 内部 ループ:

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();
        }

(各番号は 1 回ですが、もちろん順序は保証されません)

他のヒント

Pop Catalin と Marc Gravell の答えは正しいです。追加したいのはへのリンクだけです 閉店に関する私の記事 (Java と C# の両方について説明します)。少し価値を加えられるかもしれないと思っただけです。

編集:スレッドの予測不可能性がない例を挙げる価値があると思います。以下に、両方のアプローチを示す短いですが完全なプログラムを示します。「悪いアクション」リストは 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();
        }
    }
}

オプション 2 を使用する必要があります。変更する変数の周囲にクロージャを作成すると、クロージャの作成時ではなく、変数が使用されるときに変数の値が使用されます。

C# での匿名メソッドの実装とその結果 (パート 1)

C# での匿名メソッドの実装とその結果 (パート 2)

C# での匿名メソッドの実装とその結果 (パート 3)

編集:明確にするために、C# ではクロージャーは "字句閉包" これは、変数の値ではなく変数自体をキャプチャすることを意味します。つまり、変化する変数へのクロージャを作成する場合、クロージャは実際には変数の値のコピーではなく、変数への参照になります。

編集2:コンパイラの内部について知りたい人のために、すべてのブログ投稿へのリンクを追加しました。

これは興味深い質問であり、人々がさまざまな方法で答えているのを見てきたようです。私は、2 番目の方法が唯一の安全な方法であると信じていました。私は本当に簡単な証明を作りました:

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 

したがって、何も失われず、何も得られませんでした...

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top