Идентификатор foreach и замыкания



в двух следующих фрагментах, является первым безопасным или вы должны сделать второй?



под безопасным я имею в виду, что каждый поток гарантированно вызывает метод на 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();
}


обновление: как указано в ответе Джона Скита, в этом нет ничего в частности, чтобы сделать с резьбой.

449   7  

7 ответов:

Edit: это все изменения в 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#). Просто подумал, что это может добавить немного ценности.

EDIT: я думаю, что стоит привести пример, который не имеет непредсказуемости резьбы. Вот короткая, но полная программа, показывающая оба подхода. Список " плохое действие "печатает 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)

Edit: чтобы было понятно, в 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 

Так что ничего не потеряно и ничего не приобретено ...

Comments

    Ничего не найдено.