quinta-feira, 18 de agosto de 2011

Enumeração de listas com Tasks

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

domingo, 1 de maio de 2011

Acesso ao contexto em linha

Para quem utiliza Entity Framework, o acesso aos dados é fornecido pelo contexto (gerado pelo Entity Data Model Designer). Ora, o contexto gerado pela ferramenta deriva de System.Data.Objects.ObjectContext que por sua vez implementa IDisposable. Dizem as boas práticas que um objecto que seja IDisposable deve ser usado da seguinte forma:

        public IEnumerable<Product> Products { get; set; }
        void Sample1()
        {
            using (var ctx = new NorthwindEntities())
            {
                Products = ctx.Products.Where(p => p.UnitPrice < 10);
            }
        }

Simples e eficaz. Mas, agora imaginemos que quero carregar a lista de produtos num outro objecto e até a posso colocar no construtor ou na inicialização directa:

        class SomeObject { public IEnumerable<Product> Products { get; set; } }

                                                                                               
        void Sample2()
        {
            using (var ctx = new NorthwindEntities())
            {
                SomeObject someObject1 = new SomeObject()
                {
                    Products = ctx.Products.Where(p => p.UnitPrice < 10)
                };
            }
            //someObject1 not available here...
        }

Então temos um problema: é que a utilização do objecto criado fica restrita ao corpo da estrutura using. É certo que se pode declarar o objecto fora da estrutura ou simplesmente expandir a estrutura, mas isso nem sempre é desejável. Para contornar esta situação costumo utilizar um pequeno método inspirado na inversão de controlo, assim:

    public partial class NorthwindEntities
    {
        public static T Execute<T>(Func<NorthwindEntities, T> f)
        {
            using (var ctx = new NorthwindEntities())
            {
                return f(ctx);
            }
        }
    }

Neste caso, coloquei o método na própria classe de contexto, mas pode ser colocado numa qualquer classe estática. A razão de o ter colocado directamente no contexto é que é dependente do próprio contexto devido à inicialização local do mesmo. Com esta pequena ajuda, a mesma consulta pode ser escrita assim:

        void Sample3()
        {
            Products = NorthwindEntities.Execute(ctx => ctx.Products.Where(p => p.UnitPrice < 10));
        }

Ou assim:

        void Sample4()
        {
            SomeObject someObject1 = new SomeObject()
            {
                Products = NorthwindEntities.Execute(ctx => ctx.Products.Where(p => p.UnitPrice < 10))
            };
            //someObject1 available here...
        }

Claro que este exemplo apenas tem utilização quando o contexto apenas é necessário para uma única consulta. Para mais do que uma consulta deverá sempre ser utilizado o padrão standard.