Идентификатор 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();
}

Обновление. Как указано в ответе Джона Скита, это не имеет никакого отношения к потокам.


person xyz    schedule 04.02.2009    source источник
comment
На самом деле я чувствую, что это связано с потоками, как если бы вы не использовали потоки, вы бы вызывали правильного делегата. В примере Джона Скита без многопоточности проблема в том, что есть 2 цикла. Здесь только один, поэтому проблем быть не должно ... если только вы не знаете точно, когда будет выполнен код (то есть, если вы используете многопоточность - ответ Марка Гравелла прекрасно это показывает).   -  person user276648    schedule 21.12.2011
comment
возможный дубликат доступа к измененному закрытию (2)   -  person nawfal    schedule 02.11.2013
comment
@ user276648 Не требует многопоточности. Чтобы добиться такого поведения, достаточно отложить выполнение делегатов до завершения цикла.   -  person binki    schedule 20.01.2016


Ответы (7)


Изменить: все это изменения в 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();
        }

(каждый номер один раз, но порядок конечно не гарантируется)

person Marc Gravell    schedule 04.02.2009
comment
Святая корова ... этот старый пост избавил меня от головной боли. Я всегда ожидал, что область видимости переменной foreach будет находиться внутри цикла. Это был один из главных опытов с WTF. - person chris; 20.03.2012
comment
На самом деле это было сочтено ошибкой в ​​цикле foreach и исправлено в компиляторе. (В отличие от цикла for, где переменная имеет единственный экземпляр для всего цикла.) - person Ihar Bury; 21.04.2013
comment
@Orlangur Я много лет разговаривал напрямую с Эриком, Мэдсом и Андерсом. Компилятор следовал спецификации, поэтому был прав. По спец сделал выбор. Проще говоря: этот выбор был изменен. - person Marc Gravell; 21.04.2013
comment
Этот ответ применим до C # 4, но не для более поздних версий: в C # 5 переменная цикла foreach будет логически находиться внутри цикла, и поэтому каждый раз замыкания будут закрывать новую копию переменной. (Эрик Липперт) - person Douglas; 13.04.2016
comment
@ Дуглас: да, я исправлял их по ходу дела, но это было обычным камнем преткновения, так что: осталось еще немало! - person Marc Gravell; 13.04.2016

Поп Каталин и Марк Гравелл верны. Все, что я хочу добавить, это ссылку на мою статью о закрытии (в которой говорится как о 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();
        }
    }
}
person Jon Skeet    schedule 04.02.2009
comment
Спасибо - я добавил вопрос, чтобы сказать, что это не совсем про темы. - person xyz; 04.02.2009
comment
Это также было во время одной из бесед, которые вы показываете в видео на своем сайте csharpindepth.com/Talks.aspx - person missaghi; 04.02.2009
comment
Да, я, кажется, помню, что использовал там версию с потоками, и одно из предложений обратной связи заключалось в том, чтобы избегать потоков - яснее использовать пример, подобный приведенному выше. - person Jon Skeet; 04.02.2009
comment
Приятно знать, что видео смотрят :) - person Jon Skeet; 04.02.2009
comment
Даже понимание того, что переменная существует вне цикла for, меня сбивает с толку. Например, в вашем примере поведения закрытия, stackoverflow.com/a/428624/20774, переменная еще существует за пределами закрытия связывает правильно. Почему это другое? - person James McMahon; 06.04.2016
comment
@JamesMcMahon: Ничего особенного. В этом случае также фиксируется единственная переменная. По сути, вам нужно выяснить, какая переменная захватывается и где еще она используется. - person Jon Skeet; 06.04.2016
comment
О, я тебя понял. Линия записи отображает значение в данный момент, но в следующий раз, когда переменная будет изменена, она будет изменена в закрытии. Такое поведение меня просто сбивает с толку, потому что я ожидаю, что закрытие будет той ценностью, с которой оно было создано. - person James McMahon; 06.04.2016
comment
@JamesMcMahon: И в этом суть. Замыкание захватывает переменную, а не значение в момент создания. - person Jon Skeet; 07.04.2016

Вам необходимо использовать вариант 2, создание замыкания вокруг изменяющейся переменной будет использовать значение переменной при использовании переменной, а не во время создания замыкания.

Реализация анонимных методов в C # и ее последствия (часть 1) < / а>

Реализация анонимных методов в C # и ее последствия (часть 2) < / а>

Реализация анонимных методов в C # и ее последствия (часть 3) < / а>

Изменить: чтобы прояснить, в C # замыкания - это «лексические замыкания», что означает, что они захватывают не значение переменной, а саму переменную. Это означает, что при создании замыкания для изменяющейся переменной замыкание на самом деле является ссылкой на переменную, а не копией ее значения.

Edit2: добавлены ссылки на все сообщения в блоге, если кому-то интересно прочитать о внутреннем устройстве компилятора.

person Pop Catalin    schedule 04.02.2009
comment
Я думаю, это касается типов значений и ссылочных типов. - person leppie; 04.02.2009

Это интересный вопрос, и кажется, что мы видели, как люди отвечали по-разному. У меня создалось впечатление, что второй путь будет единственно безопасным. Я приготовил очень быстрое доказательство:

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 определенно небезопасен.

person JoshBerke    schedule 04.02.2009

В вашем случае вы можете избежать проблемы, не используя трюк с копированием, сопоставив свой ListOfFoo с последовательностью потоков:

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

Оба являются безопасными, начиная с C # версии 5 (.NET framework 4.5). Подробнее см. В этом вопросе: Использует foreach переменные были изменены в C # 5?

person lex82    schedule 18.12.2015

Foo f2 = f;

указывает на ту же ссылку, что и

f 

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

person Matze    schedule 04.02.2009
comment
Дело в том, что компилятор выполняет здесь некую магию, чтобы поднять локальную переменную в область неявно созданного замыкания. Вопрос в том, понимает ли компилятор, что это нужно делать для переменной цикла. Компилятор известен несколькими недостатками, касающимися подъема, так что это хороший вопрос. - person Konrad Rudolph; 04.02.2009
comment
Это не волшебство. Он просто захватывает окружающую среду. Проблема здесь и с циклами for заключается в том, что переменная захвата изменяется (повторно назначается). - person leppie; 04.02.2009
comment
leppie: компилятор генерирует код для вас, и в целом непросто увидеть, какой код именно это. Это определение магии компилятора, если она когда-либо существовала. - person Konrad Rudolph; 04.02.2009
comment
leppie: (продолжение), и поскольку другие указали, что кода 1 на самом деле недостаточно, этот пример прекрасно иллюстрирует, почему магия имени заслуженно. Интуитивно поведение должно быть таким же. Почему нет? Магия. - person Konrad Rudolph; 04.02.2009
comment
Дело не в коде, а в семантике, и они четко определены. - person leppie; 04.02.2009
comment
@konrad: моя исходная точка зрения теперь проясняет это (переменная видоизменена). Все, что вы можете себе представить, это то, что все обернуто в класс, и, как они говорят, закрытие - это классы для бедняков или что классы - это закрытие для бедняков? ;) - person leppie; 04.02.2009
comment
извините за все опечатки :( 1. надо представить 2. так говорится. - person leppie; 04.02.2009
comment
@leppie: Я здесь с Конрадом. Компилятор доходит до ощущения как по волшебству, и хотя семантика четко определена, она не совсем понятна. Какая старая поговорка о том, что что-то непонятное, сравнимо с магией? - person Jon Skeet; 04.02.2009
comment
@Jon Skeet Вы имеете в виду, что любая достаточно продвинутая технология неотличима от волшебной en.wikipedia.org/wiki/ Кларк% 27s_three_laws :) - person AwesomeTown; 04.02.2009
comment
Это не указывает на ссылку. Это ссылка. Он указывает на тот же объект, но это другая ссылка. - person mqp; 04.05.2009