web-dev-qa-db-ja.com

foreach識別子とクロージャ

次の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の回答で指摘されているように、これはスレッド化とは特に関係がありません。

77
xyz

編集:これはすべてC#5で変更され、変数が定義されている場所が変更されます(コンパイラーから見て)。 C#5以降、同じです


C#5より前

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

foreachを使用すると、変数は宣言されますoutsideループ-つまり、.

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

これは、クロージャスコープに関してfが1つしかないことを意味し、スレッドが混乱する可能性が非常に高くなります。一部のインスタンスではメソッドを複数回呼び出し、他のインスタンスではまったく呼び出しません。これは、ループ内の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回ですが、もちろん順序は保証されません)

100
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();
        }
    }
}
36
Jon Skeet

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

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

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

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

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

Edit2:コンパイラの内部について読むことに興味がある人のために、すべてのブログ投稿へのリンクを追加しました。

17
Pop Catalin

これは興味深い質問であり、人々がさまざまな方法で答えるのを見てきました。私は、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は明らかに安全ではないことがわかります。

3
JoshBerke

あなたの場合、ListOfFooを一連のスレッドにマッピングすることで、コピーのトリックを使用せずに問題を回避できます。

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

どちらもC#バージョン5(.NET Framework 4.5)の時点で安全です。詳細については、この質問を参照してください: foreachの変数の使用はC#5で変更されましたか?

0
lex82