Идентификатор foreach и замыкания - PullRequest
76 голосов
/ 04 февраля 2009

В двух следующих фрагментах безопасен ли первый или второй?? 1001 *

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

Обновление: Как указано в ответе Джона Скита, это не имеет ничего общего с многопоточностью.

Ответы [ 7 ]

100 голосов
/ 04 февраля 2009

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

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

35 голосов
/ 04 февраля 2009

Поп Каталин и Марк Гравелл ответили правильно. Все, что я хочу добавить, - это ссылка на мою статью о замыканиях (в которой говорится как о 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();
        }
    }
}
16 голосов
/ 04 февраля 2009

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

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

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

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

Редактировать: для ясности в C # замыкания являются " лексическими замыканиями ", что означает, что они не фиксируют значение переменной, а саму переменную. Это означает, что при создании замыкания для изменяющейся переменной замыкание на самом деле является ссылкой на переменную, а не копией ее значения.

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

3 голосов
/ 04 февраля 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 определенно не безопасен.

1 голос
/ 20 декабря 2011

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

var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething()));
foreach (var t in threads)
{
    t.Start();
}
0 голосов
/ 18 декабря 2015

Оба безопасны с C # версии 5 (.NET Framework 4.5). Подробности смотрите в этом вопросе: Было ли изменено использование переменных в foreach в C # 5?

0 голосов
/ 04 февраля 2009
Foo f2 = f;

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

f 

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

...