Imaginemos que num código já existente temos um método que itera uma lista e que efectua uma qualquer operação com o seu conteúdo. Neste exemplo simplesmente imprime o próprio item: static void AsyncPrint<T>(List<T> list)
{
foreach (var i in list)
{
Console.WriteLine("-> :{0}", i);
}
}
Então alguém decide que a operação que é efectuada sobre cada item pode ser feita em paralelo e nem sequer é necessário aguardar pelo resultado. O código é alterado para:
static void AsyncPrint1<T>(List<T> list)
{
foreach (var i in list)
{
Task.Factory.StartNew(() => { Console.WriteLine("-> :{0}", i); });
}
}
A questão que se coloca é: o resultado é o mesmo?
Ou melhor ainda se o programador que altera este código é fã de lambda expressions e reescreve desta forma:
static void AsyncPrint2<T>(List<T> list)
{
list.ForEach((i) =>
{
Task.Factory.StartNew(() => { Console.WriteLine("-> :{0}", i); });
});
}
O output dos métodos AsyncPrint, AsyncPrint1 e AsyncPrint2 é semelhante? Não é? Como é possível? O que está a acontecer?
Ora, o método AsyncPrint2 obtém o mesmo resultado do AsyncPrint, mas em AsyncPrint1 obtemos um resultado diferente. Tipicamente, é impresso sempre o mesmo item ou alguns são impressos em duplicado e faltam outros. Porquê? Ao iniciar uma Task dentro da enumeração tradicional do foreach e como a acção concreta está definida num delegate (ou lambda expression) contido no método, a variável i que é acedida no corpo do delegate é uma closure que revela o seu valor no momento que é acedida. Quando a lista é iterada, é dada ordem de início de uma tarefa para o tratamento do item actual e avança-se para o próximo, mas a iteração da lista continua enquanto a Thread se mantiver em execução. Ou seja, não é garantido que a tarefa se inicie no momento da sua criação e muito provavelmente, quando esta se iniciar, a enumeração já avançou e o item actual já não é o mesmo.
No método AsyncPrint2 o comportamento é o esperado porque a variável i é um parâmetro de entrada do delegate que é invocado na implementação do método List<T>.forEach(), não é uma closure.
Então, como se pode fazer para manter a utilização da expressão clássica do foreach? Simples, basta que se deixe de utilizar a variável de iteração como closure:
static void AsyncPrint3<T>(List<T> list)
{
foreach (var i in list)
{
var x = i;
Task.Factory.StartNew(() => { Console.WriteLine("-> :{0}", x); });
}
}
Já agora, também se pode utilizar a Task Parallel Library, assim:
static void AsyncPrint4<T>(List<T> list)
{
list.AsParallel().ForAll((i) =>
{
Console.WriteLine("-> :{0}", i);
});
}
No entanto, é necessário ter em atenção que aqui o comportamento será ligeiramente diferente: neste caso o método só termina quando a operação terminar em todos os itens da lista enquanto que nos exemplos anteriores isso não acontece.
Para aprender mais sobre closures recomendo este artigo do Jon Skeet: The Beauty of Closures