Paralelismo de tarefas (biblioteca paralela de tarefas)
A tarefa paralela TPL (biblioteca), como o próprio nome diz, é baseado no conceito da tarefa. O termo o paralelismo de tarefas se refere a uma ou mais tarefas independentes executando simultaneamente. Uma tarefa representa uma operação assíncrona e em algumas situações é semelhante a criação de um novo thread ou item de trabalho de ThreadPool, mas em um nível mais alto de abstração. Tarefas fornecem duas vantagens principais:
Uso mais eficiente e mais escalável de recursos do sistema.
Nos bastidores, as tarefas são enfileiradas para o ThreadPool, foi aprimorado com algoritmos (como alpinismo hill) que determinar e ajustam o número de threads que maximiza o throughput. Isso torna as tarefas relativamente simples, e você pode criar muitas para ativar o paralelismo refinado. Para complementar a isso, os algoritmos de roubo de trabalho amplamente conhecidos são empregados para fornecer balanceamento de carga.
Mais controle programático que é possível com um item de trabalho ou segmento.
Tarefas e da estrutura construída em torno deles fornecem um conjunto avançado de APIs que oferecem suporte a espera, cancelamento, continuação, manipulação de exceção robusta, status detalhado, agendamento personalizado e muito mais.
Para em ambos esses motivos, o.NET Framework 4, as tarefas são a API preferencial para escrever multi-thread, assíncrona e código paralelo.
Criando e executando tarefas implicitamente
O Parallel.Invoke método fornece uma maneira conveniente para execução simultânea de qualquer número de instruções arbitrárias. Basta passar um Action Delegar para cada item de trabalho. A maneira mais fácil de criar esses delegados é usar expressões lambda. A expressão lambda pode chamar um método nomeado ou fornecer o embutido em código. O exemplo a seguir mostra um basic Invoke chamada que cria e inicia dois as tarefas executadas simultaneamente.
Observação
Esta documentação usa expressões lambda para definir os delegados na TPL.Se você não estiver familiarizado com as expressões lambda em C# ou Visual Basic, consulte Expressões lambda no PLINQ e TPL.
Parallel.Invoke(Sub() DoSomeWork(), Sub() DoSomeOtherWork())
Parallel.Invoke(() => DoSomeWork(), () => DoSomeOtherWork());
Observação
O número de Task instâncias que são criadas nos bastidores por Invoke não é necessariamente igual ao número de representantes que são fornecidos.A TPL pode empregar várias otimizações, especialmente com um grande número de representantes.
Para obter mais informações, consulte Como: Usar Parallel. Invoke para executar operações paralelas.
Para obter maior controle sobre a execução da tarefa ou para retornar um valor da tarefa, você tem que trabalhar com Task objetos mais explicitamente.
Criando e executando tarefas explicitamente
Uma tarefa é representada pela System.Threading.Tasks.Task classe. Uma tarefa que retorna um valor é representada pela System.Threading.Tasks.Task<TResult> classe, que herda de Task. O objeto de tarefa cuida dos detalhes da infra-estrutura e fornece métodos e propriedades que são acessíveis a partir o segmento de chamada durante o tempo de vida da tarefa. Por exemplo, você pode acessar o Status propriedade de uma tarefa a qualquer momento para determinar se ele foi iniciado executando, executou a conclusão, foi cancelada ou acionou uma exceção. O status é representado por um TaskStatus enumeração.
Quando você cria uma tarefa, você atribuir um delegado de usuário que encapsula o código que a tarefa será executada. O delegado pode ser expresso como um delegado nomeado, um método anônimo ou uma expressão lambda. As expressões lambda podem conter uma chamada para um método nomeado, como mostrado no exemplo a seguir.
' Create a task and supply a user delegate by using a lambda expression.
Dim taskA = New Task(Sub() Console.WriteLine("Hello from taskA."))
' Start the task.
taskA.Start()
' Output a message from the joining thread.
Console.WriteLine("Hello from the joining thread.")
' Output:
' Hello from the joining thread.
' Hello from taskA.
// Create a task and supply a user delegate by using a lambda expression.
var taskA = new Task(() => Console.WriteLine("Hello from taskA."));
// Start the task.
taskA.Start();
// Output a message from the joining thread.
Console.WriteLine("Hello from the calling thread.");
/* Output:
* Hello from the joining thread.
* Hello from taskA.
*/
Você também pode usar o StartNew método para criar e iniciar uma tarefa em uma operação. Esta é a melhor maneira de criar e iniciar tarefas se a criação e o agendamento não precisam ser separados, conforme mostrado no exemplo a seguir
' Better: Create and start the task in one operation.
Dim taskA = Task.Factory.StartNew(Sub() Console.WriteLine("Hello from taskA."))
' Output a message from the joining thread.
Console.WriteLine("Hello from the joining thread.")
// Create and start the task in one operation.
var taskA = Task.Factory.StartNew(() => Console.WriteLine("Hello from taskA."));
// Output a message from the joining thread.
Console.WriteLine("Hello from the joining thread.");
Tarefa expõe uma propriedade estática de fábrica que retorna uma instância padrão do TaskFactory, de modo que você pode chamar o método como Task.Factory.StartNew(…). Além disso, no exemplo, porque as tarefas são do tipo System.Threading.Tasks.Task<TResult>, cada um deles tem um público Result propriedade que contém o resultado do cálculo. As tarefas executadas de forma assíncrona e podem ser concluída em qualquer ordem. Se Result é acessado antes do cálculo é concluído, a propriedade irá bloquear o thread até que o valor esteja disponível.
Dim taskArray() = {Task(Of Double).Factory.StartNew(Function() DoComputation1()),
Task(Of Double).Factory.StartNew(Function() DoComputation2()),
Task(Of Double).Factory.StartNew(Function() DoComputation3())}
Dim results() As Double
ReDim results(taskArray.Length)
For i As Integer = 0 To taskArray.Length
results(i) = taskArray(i).Result
Next
Task<double>[] taskArray = new Task<double>[]
{
Task<double>.Factory.StartNew(() => DoComputation1()),
// May be written more conveniently like this:
Task.Factory.StartNew(() => DoComputation2()),
Task.Factory.StartNew(() => DoComputation3())
};
double[] results = new double[taskArray.Length];
for (int i = 0; i < taskArray.Length; i++)
results[i] = taskArray[i].Result;
Para obter mais informações, consulte Como: Retornar um valor de uma tarefa..
Quando você usa uma expressão lambda para criar o representante de uma tarefa, você terá acesso a todas as variáveis que são visíveis nesse momento seu código-fonte. No entanto, em alguns casos, mais notavelmente dentro de loops, lambda não captura a variável como esperado. Ela captura apenas o valor final, não o valor conforme ele sofre mutações após cada iteração. Você pode acessar o valor em cada iteração, fornecendo um objeto de estado para uma tarefa por meio de seu construtor, conforme mostrado no exemplo a seguir:
Class MyCustomData
Public CreationTime As Long
Public Name As Integer
Public ThreadNum As Integer
End Class
Sub TaskDemo2()
' Create the task object by using an Action(Of Object) to pass in custom data
' in the Task constructor. This is useful when you need to capture outer variables
' from within a loop.
' As an experiement, try modifying this code to capture i directly in the lamda,
' and compare results.
Dim taskArray() As Task
ReDim taskArray(10)
For i As Integer = 0 To taskArray.Length - 1
taskArray(i) = New Task(Sub(obj As Object)
Dim mydata = CType(obj, MyCustomData)
mydata.ThreadNum = Thread.CurrentThread.ManagedThreadId
Console.WriteLine("Hello from Task #{0} created at {1} running on thread #{2}.",
mydata.Name, mydata.CreationTime, mydata.ThreadNum)
End Sub,
New MyCustomData With {.Name = i, .CreationTime = DateTime.Now.Ticks}
)
taskArray(i).Start()
Next
End Sub
class MyCustomData
{
public long CreationTime;
public int Name;
public int ThreadNum;
}
void TaskDemo2()
{
// Create the task object by using an Action(Of Object) to pass in custom data
// in the Task constructor. This is useful when you need to capture outer variables
// from within a loop. As an experiement, try modifying this code to
// capture i directly in the lambda, and compare results.
Task[] taskArray = new Task[10];
for(int i = 0; i < taskArray.Length; i++)
{
taskArray[i] = new Task((obj) =>
{
MyCustomData mydata = (MyCustomData) obj;
mydata.ThreadNum = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine("Hello from Task #{0} created at {1} running on thread #{2}.",
mydata.Name, mydata.CreationTime, mydata.ThreadNum)
},
new MyCustomData () {Name = i, CreationTime = DateTime.Now.Ticks}
);
taskArray[i].Start();
}
}
Esse estado é passado como um argumento para o delegado da tarefa e é acessível a partir do objeto de tarefa usando o AsyncState propriedade. Além disso, passando dados por meio do construtor pode fornecer um pequeno benefício no desempenho em alguns cenários.
Identificação da tarefa
Cada tarefa recebe um ID de inteiro que o identifica exclusivamente em um domínio de aplicativo e que pode ser acessado usando o Id propriedade. A identificação é útil para exibir informações sobre a tarefa no depurador Visual Studio Paralela pilhas e Tarefas paralelas windows. A identificação ociosamente é criada, o que significa que ele não será criado até que ela é solicitada; Portanto, uma tarefa pode ter uma identificação diferente cada vez que o programa é executado. Para obter mais informações sobre como exibir as identificações de tarefas no depurador, consulte Usando a janela de pilhas paralela.
Opções de criação de tarefa
A maioria das APIs que criar tarefas fornecem sobrecargas que aceitam uma TaskCreationOptions parâmetro. Especificando uma dessas opções, você pode instruir o Agendador de tarefas como agendar a tarefa no pool de segmentos. A tabela a seguir lista as várias opções de criação de tarefa.
Elemento |
Descrição |
---|---|
None |
A opção padrão quando nenhuma opção for especificada. O Agendador usa a heurística de padrão para agendar a tarefa. |
PreferFairness |
Especifica que a tarefa deve ser agendada para que as tarefas criadas mais cedo será mais prováveis a ser executado mais cedo, e as tarefas criadas posteriormente serão mais probabilidade de ser executado posteriormente. |
LongRunning |
Especifica que a tarefa representa uma operação longa.. |
AttachedToParent |
Especifica que uma tarefa deve ser criada como um filho anexado à tarefa atual, se houver. Para obter mais informações, consulte Aninhados de tarefas e tarefas filho. |
As opções podem ser combinadas usando um operador bit a bit OR operação. O exemplo a seguir mostra uma tarefa que tem o LongRunning e PreferFairness opção.
Dim task3 = New Task(Sub() MyLongRunningMethod(),
TaskCreationOptions.LongRunning Or TaskCreationOptions.PreferFairness)
task3.Start()
var task3 = new Task(() => MyLongRunningMethod(),
TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness);
task3.Start();
Criando a continuação da tarefa
O Task.ContinueWith método e Task<TResult>.ContinueWith método permitem que você especificar uma tarefa a ser iniciado quando o tarefa antecedente for concluída. Representante da tarefa de continuação é passada uma referência antecedent, para que ele possa examinar o seu status. Além disso, um valor definido pelo usuário pode ser passado de antecedent com a sua continuação na Result propriedade, para que a saída de antecedent pode servir como entrada para a continuação. No exemplo a seguir, getData é iniciado pelo código de programa, em seguida, analyzeData é iniciado automaticamente quando getData for concluída, e reportData é iniciado quando analyzeData conclui. getDataproduz como seu resultado em uma matriz de bytes é passada para analyzeData. analyzeDataprocessa a matriz e retorna um resultado cujo tipo é inferido do tipo de retorno de Analyze método. reportDataobtém dados de entrada do analyzeDatae produz um resultado cujo tipo é inferido de maneira semelhante, e que é disponibilizado para o programa de Result propriedade.
Dim getData As Task(Of Byte()) = New Task(Of Byte())(Function() GetFileData())
Dim analyzeData As Task(Of Double()) = getData.ContinueWith(Function(x) Analyze(x.Result))
Dim reportData As Task(Of String) = analyzeData.ContinueWith(Function(y As Task(Of Double)) Summarize(y.Result))
getData.Start()
System.IO.File.WriteAllText("C:\reportFolder\report.txt", reportData.Result)
Task<byte[]> getData = new Task<byte[]>(() => GetFileData());
Task<double[]> analyzeData = getData.ContinueWith(x => Analyze(x.Result));
Task<string> reportData = analyzeData.ContinueWith(y => Summarize(y.Result));
getData.Start();
//or...
Task<string> reportData2 = Task.Factory.StartNew(() => GetFileData())
.ContinueWith((x) => Analyze(x.Result))
.ContinueWith((y) => Summarize(y.Result));
System.IO.File.WriteAllText(@"C:\reportFolder\report.txt", reportData.Result);
O ContinueWhenAll e ContinueWhenAny métodos permitem que você continue a partir de várias tarefas. Para obter mais informações, consulte Tarefas de continuação e Como: A cadeia de várias tarefas com continuação.
Criando desanexado tarefas aninhadas
Quando o código de usuário que está executando uma tarefa cria uma nova tarefa e não especificar o AttachedToParent opção, a nova tarefa não sincronizado com a tarefa externa de qualquer maneira especial. Essas tarefas são chamadas de um desanexado tarefa aninhada. O exemplo a seguir mostra uma tarefa que cria uma tarefa de aninhados desanexada.
Dim outer = Task.Factory.StartNew(Sub()
Console.WriteLine("Outer task beginning.")
Dim child = Task.Factory.StartNew(Sub()
Thread.SpinWait(5000000)
Console.WriteLine("Detached task completed.")
End Sub)
End Sub)
outer.Wait()
Console.WriteLine("Outer task completed.")
' Output:
' Outer task beginning.
' Outer task completed.
' Detached child completed.
var outer = Task.Factory.StartNew(() =>
{
Console.WriteLine("Outer task beginning.");
var child = Task.Factory.StartNew(() =>
{
Thread.SpinWait(5000000);
Console.WriteLine("Detached task completed.");
});
});
outer.Wait();
Console.WriteLine("Outer task completed.");
/* Output:
Outer task beginning.
Outer task completed.
Detached task completed.
*/
Observe que a tarefa externa não aguarda a conclusão da tarefa aninhada.
Criando tarefas filho
Quando o código de usuário que está executando uma tarefa cria uma tarefa com o AttachedToParent opção, a nova tarefa é conhecida como uma tarefa filho da tarefa original, que é conhecida como o pai de tarefas. Você pode usar o AttachedToParent a opção para o paralelismo de tarefas estruturadas de express, porque a tarefa pai aguarda implicitamente todas as tarefas filho concluir. O exemplo a seguir mostra uma tarefa que cria uma tarefa do filho:
Dim parent = Task.Factory.StartNew(Sub()
Console.WriteLine("Parent task beginning.")
Dim child = Task.Factory.StartNew(Sub()
Thread.SpinWait(5000000)
Console.WriteLine("Attached child completed.")
End Sub,
TaskCreationOptions.AttachedToParent)
End Sub)
outer.Wait()
Console.WriteLine("Parent task completed.")
' Output:
' Parent task beginning.
' Attached child completed.
' Parent task completed.
var parent = Task.Factory.StartNew(() =>
{
Console.WriteLine("Parent task beginning.");
var child = Task.Factory.StartNew(() =>
{
Thread.SpinWait(5000000);
Console.WriteLine("Attached child completed.");
}, TaskCreationOptions.AttachedToParent);
});
parent.Wait();
Console.WriteLine("Parent task completed.");
/* Output:
Parent task beginning.
Attached task completed.
Parent task completed.
*/
Para obter mais informações, consulte Aninhados de tarefas e tarefas filho.
Espera-se em tarefas
O System.Threading.Tasks.Task tipo e System.Threading.Tasks.Task<TResult> tipo fornecem várias sobrecargas de um Task.Wait e Task<TResult>.Wait método permitem que você aguarde a conclusão de uma tarefa. Além disso, sobrecargas de estática Task.WaitAll método e Task.WaitAny método permitem que você espera para qualquer ou todos de uma matriz de tarefas para concluir.
Normalmente, você deve aguardar para uma tarefa um desses motivos:
O thread principal depende do resultado final calculado por uma tarefa.
Você tem que lidar com exceções que podem ser geradas de tarefa.
O exemplo a seguir mostra o padrão básico que não envolve a manipulação de exceção.
Dim tasks() =
{
Task.Factory.StartNew(Sub() MethodA()),
Task.Factory.StartNew(Sub() MethodB()),
Task.Factory.StartNew(Sub() MethodC())
}
' Block until all tasks complete.
Task.WaitAll(tasks)
' Continue on this thread...
Task[] tasks = new Task[3]
{
Task.Factory.StartNew(() => MethodA()),
Task.Factory.StartNew(() => MethodB()),
Task.Factory.StartNew(() => MethodC())
};
//Block until all tasks complete.
Task.WaitAll(tasks);
// Continue on this thread...
Para obter um exemplo que mostra o tratamento de exceções, consulte Como: Manipular exceções lançadas por tarefas.
Algumas sobrecargas que permitem que você especifique um tempo limite e outras pessoas tenham adicional CancellationToken como um parâmetro de entrada, para que a espera em si pode ser cancelada programaticamente ou em resposta a entrada do usuário.
Quando você esperar em uma tarefa, você espera implicitamente em todos os filhos dessa tarefa foram criados usando o TaskCreationOptions AttachedToParent opção. Task.WaitRetorna imediatamente se a tarefa já foi concluída. As exceções geradas por uma tarefa serão lançadas por um Wait método, mesmo se o Wait o método foi chamado após a tarefa foi concluída.
Para obter mais informações, consulte Como: Espera-se em uma ou mais tarefas para concluir.
Tratamento de exceções em tarefas
Quando uma tarefa lança uma ou mais exceções, as exceções são encapsuladas em um AggregateException. Essa exceção é propagada de volta para o segmento que une com a tarefa, que é normalmente o segmento que está aguardando a tarefa ou tenta acessar a propriedade do resultado da tarefa. Esse comportamento serve para impor o.Diretiva do NET Framework que todas as exceções não tratadas por padrão deve ter subdividir o processo. O código de chamada pode lidar com as exceções usando o Wait, WaitAll, ou WaitAny método ou a Result() propriedade sobre a tarefa ou o grupo de tarefas e colocando-o Wait método em um try-catch block.
O segmento de ingresso também pode lidar com exceções, acessando o Exception propriedade antes que a tarefa seja coletado ao lixo. Acessando essa propriedade, você pode impedir que a exceção não tratada não aciona o comportamento de propagação de exceção que destrói o processo, quando o objeto é finalizado.
Para obter mais informações sobre exceções e tarefas, consulte (Biblioteca paralela de tarefas) de manipulação de exceção e Como: Manipular exceções lançadas por tarefas.
Cancelando tarefas
O Task classe oferece suporte ao cancelamento cooperativo e está totalmente integrado com o System.Threading.CancellationTokenSource classe e o System.Threading.CancellationToken classe, que são novos na.NET Framework versão 4. Muitos dos construtores da System.Threading.Tasks.Task classe tirar uma CancellationToken como um parâmetro de entrada. Muitos da StartNew sobrecargas também levam um CancellationToken.
Você pode criar o token e emitir a solicitação de cancelamento tarde, usando o CancellationTokenSource classe. Passar o token para o Task como um argumento e também referência o mesmo token no seu representante do usuário, que funciona de responder a uma solicitação de cancelamento. Para obter mais informações, consulte Cancelamento da tarefa e Como: Cancelar uma tarefa e seus filhos.
A classe TaskFactory
O TaskFactory classe fornece métodos estáticos que encapsulam alguns padrões comuns para criar e iniciar as tarefas e tarefas de continuação.
O padrão mais comum é StartNew, que cria e inicia uma tarefa em uma instrução. Para obter mais informações, consulte StartNew().
Quando você criar tarefas de continuação de vários antecedentes, use o ContinueWhenAll método ouContinueWhenAnymétodo ou seus equivalentes na Task<TResult> classe. Para obter mais informações, consulte Tarefas de continuação.
Encapsular o modelo de programação assíncrona BeginX e EndX métodos em um Task ou Task<TResult> de instância, use o FromAsync métodos. Para obter mais informações, consulte A TPL e tradicionais.NET programação assíncrona.
O padrão TaskFactory está acessível como uma propriedade estática na Task classe ou Task<TResult> classe. Você também pode instanciar um TaskFactory diretamente e especificar várias opções que incluem um CancellationToken, um TaskCreationOptions opção, um TaskContinuationOptions opção, ou um TaskScheduler. Quaisquer opções especificadas quando você criar a fábrica de tarefa serão aplicadas a todas as tarefas que ele cria, a menos que a tarefa é criada usando o TaskCreationOptions substituição de opções da tarefa de caso a enumeração, em que aqueles da fábrica de tarefa.
Tarefas sem delegados
Em alguns casos, convém usar um Task para encapsular alguma operação assíncrona, que é executada por um componente externo em vez do próprio usuário delegado. Se a operação for baseada no padrão assíncrono de programação modelo Begin/End, você pode usar o FromAsync métodos. Se não for o caso, você pode usar o TaskCompletionSource<TResult> objeto para encapsular a operação de uma tarefa e assim assumir algumas das vantagens do Task suporte de programação, por exemplo, para a propagação de exceção e continuação. Para obter mais informações, consulte TaskCompletionSource<TResult>.
Agendadores personalizados
A maioria dos desenvolvedores de aplicativo ou de biblioteca não se preocupar com qual processador a tarefa é executada em como ele sincroniza o seu trabalho com outras tarefas ou como ele é programado na System.Threading.ThreadPool. Eles só exigem que ele executar o modo mais eficiente possível no computador host. Se você precisar de mais um controle refinado sobre os detalhes do agendamento, a biblioteca paralela de tarefas permite definir algumas configurações no Agendador de tarefas padrão e até mesmo permite que você forneça um Agendador personalizado. Para obter mais informações, consulte TaskScheduler.
Estruturas de dados relacionados
A TPL tem vários novos tipos públicos que são úteis em situações de paralelas e seqüenciais. Esses incluem várias classes de coleção de thread-safe, rápido e escalável no System.Collections.Concurrent namespace e vários novos tipos de sincronização, por exemplo, SemaphoreLock e System.Threading.ManualResetEventSlim, que são mais eficientes do que seus antecessores para tipos específicos de cargas de trabalho. Outros tipos de novos na.NET Framework versão 4, por exemplo, System.Threading.Barrier e System.Threading.SpinLock, que fornecem funcionalidade que foi não está disponível em versões anteriores. Para obter mais informações, consulte Estruturas de dados para a programação paralela.
Tipos de tarefas personalizados
Recomendamos que você não herda de System.Threading.Tasks.Task ou System.Threading.Tasks.Task<TResult>. Em vez disso, use o AsyncState a propriedade para associar dados adicionais ou estado com um Task ou Task<TResult> objeto. Você também pode usar os métodos de extensão para estender a funcionalidade da Task e Task<TResult> classes. Para obter mais informações sobre os métodos de extensão, consulte Métodos de extensão (guia de programação TRANSLATION FROM VPE FOR CSHARP) e Métodos de extensão (Visual Basic).
Se você deve herdar de Task ou Task<TResult>, você não pode usar o System.Threading.Tasks.TaskFactory, System.Threading.Tasks.TaskFactory<TResult>, ou System.Threading.Tasks.TaskCompletionSource<TResult> classes para criar instâncias do seu tipo de tarefas personalizado, porque essas classes criar somente Task e Task<TResult> objetos. Além disso, você não pode usar os mecanismos de continuação de tarefa são fornecidos por Task, Task<TResult>, TaskFactory, e TaskFactory<TResult> para criar instâncias do seu tipo de tarefas personalizado, porque esses mecanismos também criar somente Task e Task<TResult> objetos.
Tópicos relacionados
Título |
Descrição |
Descreve como funciona a continuação. |
|
Descreve a diferença entre tarefas filho e tarefas aninhadas. |
|
Descreve o suporte de cancelamento é incorporado a Task classe. |
|
Descreve como exceções de segmentos simultâneos são manipuladas. |
|
Como: Usar Parallel. Invoke para executar operações paralelas |
Descreve como usar Invoke. |
Descreve como valores de retorno das tarefas. |
|
Descreve como tarefas de espera. |
|
Descreve como Cancelar tarefas. |
|
Descreve como manipular exceções lançadas por tarefas. |
|
Descreve como executar uma tarefa quando outra tarefa é concluída. |
|
Descreve como usar tarefas para percorrer uma árvore binária. |
|
Descreve como usar For e ForEach para criar loops paralelos sobre dados. |
|
Nó de nível superior para.NET programação paralela. |
Consulte também
Conceitos
Programação em paralela a.NET Framework
Histórico de alterações
Date |
History |
Motivo |
---|---|---|
Março de 2011 |
Adicionadas informações sobre como herdar de Task e Task<TResult> classes. |
Aprimoramento de informações. |