O identificador foreach e fechamentos
-
21-08-2019 - |
Pergunta
Nos dois trechos seguintes, é o primeiro seguro um ou você deve fazer o segundo?
Por meio seguro que é cada thread garantido para chamar o método na Foo a partir da mesma iteração do loop, em que o fio foi criado?
Ou deve copiar a referência a uma nova variável "local" para cada iteração do loop?
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();
}
Update:. Como apontado na resposta de Jon Skeet, isso não tem especificamente a ver com rosca
Solução
Edit: Isso tudo muda em C # 5, com uma alteração em que a variável é definida (aos olhos do compilador). De C # 5 em diante, eles são os mesmos .
Antes de C # 5
O segundo é seguro; o primeiro não é.
Com foreach
, a variável é declarada fora o loop -. I
Foo f;
while(iterator.MoveNext())
{
f = iterator.Current;
// do something with f
}
Isto significa que há apenas 1 f
em termos de escopo de encerramento, e os fios pode muito provavelmente ficar confuso - chamar o método várias vezes em alguns casos e não em todos os outros. Você pode corrigir isso com uma segunda declaração da variável dentro do loop:
foreach(Foo f in ...) {
Foo tmp = f;
// do something with tmp
}
Isso, então, tem uma tmp
separado em cada escopo de encerramento, portanto, não há risco de esta questão.
Aqui está uma prova simples do problema:
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();
}
Saídas (aleatoriamente):
1
3
4
4
5
7
7
8
9
9
Adicionar uma variável temporário e ele funciona:
foreach (int i in data)
{
int j = i;
new Thread(() => Console.WriteLine(j)).Start();
}
(cada número uma vez, mas é claro que a ordem não é garantido)
Outras dicas
Pop Catalin e respostas de Marc Gravell estão corretas. Tudo o que eu quero adicionar um link para meu artigo sobre fechamentos (que fala sobre ambos Java e C#). Apenas pensei que poderia adicionar um pouco de valor.
EDIT: Eu acho que vale a pena dar um exemplo que não tem a imprevisibilidade de threading. Aqui está um programa curto, mas completo, mostrando ambas as abordagens. Os "bad ação" lista imprime 10 de dez vezes; a "boa ação" lista conta de 0 a 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();
}
}
}
A sua necessidade opção utilização 2 a, criando um fechamento em torno de uma variável mudança vai usar o valor da variável quando a variável é utilizada e não no momento da criação de fechamento.
A implementação de métodos anônimos em C # e suas conseqüências (parte 1)
A implementação de métodos anônimos em C # e suas conseqüências (parte 2)
A implementação de métodos anônimos em C # e suas conseqüências (parte 3)
Edit: para deixar claro, em C # fechamentos são " lexical fechamento " o que significa que não captam o valor de uma variável, mas a própria variável. Isso significa que quando a criação de um fecho para uma variável mudar o fechamento é realmente uma referência para a variável não uma cópia do seu valor.
Edit2:. Ligações adicionado a todas as mensagens de blog se alguém estiver interessado em ler sobre o funcionamento interno do compilador
Esta é uma questão interessante e parece que temos visto pessoas respondem em todas as várias maneiras. Fiquei com a impressão de que a segunda maneira seria a única maneira segura. Eu chicoteado uma prova rápida real:
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();
}
}
}
Se você executar isso, você verá a opção 1 é definitivamente não é seguro.
No seu caso, você pode evitar o problema sem usar o truque de copiar mapeando seu ListOfFoo
a uma sequência de tópicos:
var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething()));
foreach (var t in threads)
{
t.Start();
}
Ambos são seguros a partir de C # versão 5 (framework .NET 4.5). Veja esta questão para mais detalhes: tem o uso de foreach de variáveis ??foram alteradas em C # 5?
Foo f2 = f;
aponta para a mesma referência como
f
Portanto, nada perdido e nada ganho ...