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

В двух после отрывков первый безопасен, или необходимо ли сделать второй?

Сейфом я подразумеваю, что каждый поток, как гарантируют, назовет метод на Нечто от того же повторения цикла, в котором был создан поток?

Или необходимо ли скопировать ссылку на новую переменную, "локальную" для каждого повторения цикла?

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, это не имеет ничего конкретно, чтобы сделать с поточной обработкой.

78
задан abatishchev 17 December 2014 в 18:45
поделиться

5 ответов

Редактирование: это все изменяется в C# 5 с изменением туда, где переменная определяется (в глазах компилятора). От [1 119] 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();
        }

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

102
ответ дан itsrizi 24 November 2019 в 10:33
поделиться

Pop Catalin и ответы Marc Gravell корректны. Все, что я хочу добавить, является ссылкой на моя статья о закрытиях (который говорит и о 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();
        }
    }
}
37
ответ дан Jon Skeet 24 November 2019 в 10:33
поделиться

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

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

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

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

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

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

17
ответ дан Pop Catalin 24 November 2019 в 10:33
поделиться

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

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 24 November 2019 в 10:33
поделиться
Foo f2 = f;

точки к той же ссылке как

f 

, Таким образом, ничто не проиграло и ничто полученное...

-5
ответ дан Matze 24 November 2019 в 10:33
поделиться
Другие вопросы по тегам:

Похожие вопросы: