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

Foi útil?

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 ...

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top