Tarefas assíncronas

Simplifique a programação assíncrona com tarefas

Igor Ostrovsky

Baixar os códigos de exemplo

Programação assíncrona é um conjunto de técnicas para implementar operações caras que são executadas simultaneamente com o restante do programa. Um domínio em que a programação assíncrona é utilizada com frequência é no contexto de programas com uma interface gráfica do usuário: geralmente é inaceitável quando a interface do usuário congela durante a execução de uma operação dispendiosa. Além disso, as operações assíncronas são importantes para aplicativos de servidor que precisam lidar com várias solicitações de cliente ao mesmo tempo.

Exemplos representativos de operações assíncronas que ocorrem na prática incluem enviar uma solicitação para um servidor e aguardar a resposta, ler dados no disco rígido e executar um cálculo complexo, como uma verificação ortográfica.

Considere o exemplo de um aplicativo que possui uma interface do usuário. O aplicativo poderia ser compilado com o Windows Presentation Foundation (WPF) ou o Windows Forms. Nesse aplicativo, a maior parte do código é executada no thread de interface do usuário, pois ele executa os manipuladores de eventos referentes a eventos que se originam dos controles de interface. Quando o usuário clicar em um botão, o thread de interface do usuário selecionará a mensagem e executará o manipulador de eventos Click.

Agora, imagine que, no manipulador de eventos Click, o aplicativo envia uma solicitação para um servidor e aguarda uma resposta:

// !!! Bad code !!!
void Button_Click(object sender, RoutedEventArgs e) {
  WebClient client = new WebClient();
  client.DownloadFile("http://www.microsoft.com", "index.html");
}

Há um grande problema nesse código: o download de um site pode levar vários segundos ou mais. Por outro lado, a chamada a Button_Click pode levar vários segundos para retornar. Isso significa que o thread de interface do usuário será bloqueado por vários segundos e a interface do usuário será congelada. Uma interface congelada leva a uma experiência de usuário insatisfatória e quase sempre é inaceitável.

Para manter a interface do usuário do aplicativo responsiva até que o servidor responda, é importante que o download não seja uma operação síncrona no thread de interface do usuário.

Vamos tentar corrigir o problema da interface do usuário congelada. Uma solução possível, mas não ideal, é comunicar-se com o servidor em outro thread para que o thread de interface do usuário permaneça desbloqueado. Este é um exemplo que usa um thread do pool de threads para conversar com o servidor:

// Suboptimal code
void Button_Click(object sender, RoutedEventArgs e) {
  ThreadPool.QueueUserWorkItem(_ => {
    WebClient client = new WebClient();
    client.DownloadFile(
      "http://www.microsoft.com", "index.html");
  });
}

Esse código de exemplo corrige o problema da primeira versão: agora o evento Button_Click não bloqueia o thread de interface do usuário, mas a solução baseada em thread tem três problemas consideráveis. Vamos examiná-los mais de perto.

Problema 1: Threads desperdiçados do pool de threads

A correção que acabei de demonstrar usa um thread do pool de threads para enviar uma solicitação ao servidor e aguarda até que ele responda.

O thread do pool de threads vai ficar parado bloqueado até o servidor responder. O thread não pode ser retornado para o pool até que a chamada para WebClient.DownloadFile seja concluída. Bloquear um thread do pool de threads é bem melhor do que bloquear o thread de interface do usuário, pois a interface do usuário não será congelada, mas isso de fato gasta um thread do pool de threads.

Se o seu aplicativo eventualmente bloqueia um thread do pool de threads por um instante, a desvantagem de desempenho pode ser insignificante. Caso o seu aplicativo faça muito isso, a capacidade de resposta poderá diminuir devido à pressão sobre o pool de threads. O pool de threads tentará enfrentar isso criando mais threads, mas isso representa um custo de desempenho considerável.

Todos os outros padrões de programação assíncrona apresentados neste artigo corrigem o problema de threads desperdiçados do pool de threads.

Problema 2: Retornando o resultado

Existe outra dificuldade relacionada ao uso de threads para programação assíncrona: retornar um valor da operação que foi executada no thread auxiliar é um processo um pouco confuso.

No exemplo inicial, o método DownloadFile grava a página Web baixada em um arquivo local e, por isso, tem um valor de retorno nulo. Considere outra versão do problema — em vez de gravar a página Web baixada em um arquivo, você deseja atribuir o HTML recebido para a propriedade Text de um TextBox (chamado HtmlTextBox).

Uma forma ingênua, e errada, de implementar isso seria fazendo o seguinte:

// !!! Broken code !!!
void Button_Click(object sender, RoutedEventArgs e) {
  ThreadPool.QueueUserWorkItem(_ => {
    WebClient client = new WebClient();
    string html = client.DownloadString(
      "http://www.microsoft.com", "index.html");
    HtmlTextBox.Text = html;
  }); 
}

O problema é que um controle da interface do usuário (HtmlTextBox) está sendo modificado em um thread do pool de threads. Isso é um erro porque somente o thread de interface do usuário pode modificar a interface. Esta restrição está presente tanto no WPF quanto no Windows Forms, por muito bons motivos.

Para corrigir esse problema, é possível capturar o contexto de sincronização no thread de interface do usuário e, em seguida, postar uma mensagem para ele no thread do pool de threads:

void Button_Click(object sender, RoutedEventArgs e) {
  SynchronizationContext ctx = SynchronizationContext.Current;
  ThreadPool.QueueUserWorkItem(_ => {
    WebClient client = new WebClient();
    string html = client.DownloadString(
      "http://www.microsoft.com");
    ctx.Post(state => {
      HtmlTextBox.Text = (string)state;
    }, html);
  });
}

É importante reconhecer que o problema de retornar um valor de um thread auxiliar não se limita a aplicativos que possuem interfaces de usuário. De um modo geral, retornar um valor de um thread para outro é um problema complicado que exige o uso de primitivos de sincronização.

Problema 3: Compondo operações assíncronas

Trabalhar explicitamente com threads também dificulta a composição de operações assíncronas. Por exemplo, para baixar várias páginas Web em paralelo, o código de sincronização fica ainda mais difícil de escrever e mais suscetível a erros.

Tal implementação manteria um contador das operações assíncronas que ainda estão sendo executadas. O contador teria de ser modificado de uma forma segura para o thread, por exemplo, usando Interlocked.Decrement. Quando o contador chegasse ao zero, o código que processa os downloads seria executado. Tudo isso resulta em um volume incomum de código que pode facilmente gerar um erro.

É desnecessário dizer que se torna mais difícil ainda implementar corretamente um padrão de composição mais complicado usando o padrão baseado em thread.

Padrão baseado em evento

Um padrão comum para programação assíncrona com o Microsoft .NET Framework é o modelo baseado em evento. O modelo de evento expõe um método para iniciar a operação assíncrona e gera um evento quando a operação é concluída.

O padrão de evento é uma convenção para expor operações assíncronas, mas não consiste em um contrato explícito, como por meio de uma interface. O implementador de classe pode decidir com que fidelidade ele seguirá o padrão. A Figura 1 mostra um exemplo de métodos expostos por uma implementação correta do padrão de programação assíncrona baseado em evento.

Figura 1 Métodos para um padrão baseado em evento

public class AsyncExample {
  // Synchronous methods.
  public int Method1(string param);
  public void Method2(double param);

  // Asynchronous methods.
  public void Method1Async(string param);
  public void Method1Async(string param, object userState);
  public event Method1CompletedEventHandler Method1Completed;

  public void Method2Async(double param);
  public void Method2Async(double param, object userState);
  public event Method2CompletedEventHandler Method2Completed;

  public void CancelAsync(object userState);

  public bool IsBusy { get; }

  // Class implementation not shown.
  ...
}

WebClient é uma classe do .NET Framework que implementa operações assíncronas através do padrão baseado em evento. Para fornecer uma variante assíncrona do método DownloadString, WebClient expõe os métodos DownloadStringAsync e CancelAsync e o evento DownloadStringCompleted. É assim que o nosso exemplo seria implementado de uma forma assíncrona:

void Button_Click(object sender, RoutedEventArgs e) {
  WebClient client = new WebClient();
  client.DownloadStringCompleted += eventArgs => {
      HtmlTextBox.Text = eventArgs.Result;
  };
  client.DownloadStringAsync("http://www.microsoft.com");
}

Esta implementação resolve o Problema 1 da solução ineficiente baseada em thread: bloqueio desnecessário de threads. A chamada para DownloadStringAsync retorna imediatamente e não bloqueia o thread de interface do usuário nem o thread do pool de threads. O download ocorre em segundo plano e, uma vez concluído, o evento DownloadStringCompleted será executado no thread apropriado.

Observe que o manipulador de eventos DownloadStringCompleted é executado no thread correto, sem a necessidade do SynchronizationContext do qual precisei na solução baseada em thread. Em segundo plano, WebClient automaticamente captura o SynchronizationContext e posta o retorno de chamada no contexto. As classes que implementam o padrão baseado em evento normalmente se certificarão de que o manipulador Completed seja executado no thread correto.

O padrão de programação assíncrona baseado em evento é eficiente por não bloquear mais threads do que o necessário, além de ser um dos dois padrões amplamente utilizados no .NET Framework. No entanto, esse padrão tem muitas limitações:

  • O padrão é informal e somente por convenção — classes podem se desviar dele.
  • Pode ser muito difícil compor várias operações assíncronas, como processar operações assíncronas iniciadas em paralelo ou lidar com uma sequência de operações assíncronas.
  • Não é possível sondar para saber se a operação assíncrona foi concluída.
  • Deve-se tomar muito cuidado ao utilizar esses tipos. Por exemplo, se uma instância for usada para processar várias operações assíncronas, um manipulador de eventos registrado deverá ser codificado para lidar somente com a operação assíncrona desejada, mesmo que seja chamado várias vezes.
  • Os manipuladores de eventos sempre serão chamados na SynchronizationContext capturada quando a operação assíncrona for iniciada, mesmo que a execução no thread de interface do usuário seja desnecessária, o que gera mais custos de desempenho.
  • Pode ser difícil fazer a implementação corretamente e é necessário definir vários tipos (por exemplo, manipuladores de eventos ou argumentos de evento).

A Figura 2 lista diversos exemplos de classes do .NET Framework 4 que implementam o padrão assíncrono baseado em evento.

Figura 2 Exemplos do padrão assíncrono baseado em evento em classes do .NET

Classe Operação
System.Activities.WorkflowInvoker InvokeAsync
System.ComponentModel.BackgroundWorker RunWorkerAsync
System.Net.Mail.SmtpClient SendAsync
System.Net.NetworkInformation.Ping SendAsync
System.Net.WebClient DownloadStringAsync

Padrão IAsyncResult

Outra convenção para implementar operações assíncronas no .NET é o padrão IAsyncResult. Em comparação com o modelo baseado em evento, IAsyncResult é uma solução mais avançada para a programação assíncrona.

No padrão IAsyncResult, uma operação assíncrona é exposta usando métodos Begin e End. Chame o método Begin para iniciar a operação assíncrona e passe um delegado que será chamado quando a operação for concluída. A partir do retorno de chamada, você chama o método End, que retorna o resultado da operação assíncrona. Como alternativa, em vez de fornecer um retorno de chamada, você pode sondar se a operação foi concluída ou aguardar o resultado de modo síncrono.

Como exemplo, considere o método Dns.GetHostAddresses, que aceita um nome de host e retorna uma matriz de endereços IP para os quais o nome de host é resolvido. A assinatura da versão síncrona do método é parecida com o seguinte:

public static IPAddress[] GetHostAddresses(
  string hostNameOrAddress)
The asynchronous version of the method is exposed as follows:
public static IAsyncResult BeginGetHostAddresses(
  string hostNameOrAddress,
  AsyncCallback requestCallback,
  Object state)

public static IPAddress[] EndGetHostAddresses(
  IAsyncResult asyncResult)

Este é um exemplo que usa os métodos BeginGetHostAddresses e EndGetHostAddresses para consultar assincronamente o DNS do endereço www.microsoft.com:

static void Main() {
  Dns.BeginGetHostAddresses(
    "www.microsoft.com",
    result => {
      IPAddress[] addresses = Dns.EndGetHostAddresses(result);
      Console.WriteLine(addresses[0]);
    }, 
    null);
  Console.ReadKey();
}

A Figura 3 lista várias classes do .NET que implementam uma operação assíncrona utilizando o padrão baseado em evento. Comparando as Figuras 2 e 3, você perceberá que algumas classes implementam o padrão baseado em evento, outras implementam o padrão IAsyncResult e outras ainda implementam os dois.

Figura 3 Exemplos de IAsyncResult em classes do .NET

Classe Operação
System.Action BeginInvoke
System.IO.Stream BeginRead
System.Net.Dns BeginGetHostAddresses
System.Net.HttpWebRequest BeginGetResponse
System.Net.Sockets.Socket BeginSend
System.Text.RegularExpressions.MatchEvaluator BeginInvoke
System.Data.SqlClient.SqlCommand BeginExecuteReader
System.Web.DefaultHttpHandler BeginProcessRequest

Do ponto de vista histórico, o padrão IAsyncResult foi introduzido no .NET Framework 1.0 como uma abordagem de alto desempenho para a implementação de APIs assíncronas. Contudo, ele requer um trabalho extra para interagir com o thread de interface do usuário, é difícil de implementar corretamente e pode ser difícil de consumir. O padrão baseado em evento foi introduzido no .NET Framework 2.0 para atenuar aspectos da interface do usuário que não foram resolvidos por IAsyncResult e enfoca principalmente cenários onde um aplicativo com interface do usuário inicia um único aplicativo assíncrono e depois trabalha com ele.

Padrão de tarefa

Um novo tipo, chamado System.Threading.Tasks.Task, foi introduzido no .NET Framework 4 como forma de representar operações assíncronas. Uma tarefa pode representar um cálculo comum executado em uma CPU:

static void Main() {
  Task<double> task = Task.Factory.StartNew(() => { 
    double result = 0; 
    for (int i = 0; i < 10000000; i++) 
      result += Math.Sqrt(i);
    return result;
  });

  Console.WriteLine("The task is running asynchronously...");
  task.Wait();
  Console.WriteLine("The task computed: {0}", task.Result);
}

Tarefas criadas usando o método StartNew correspondem a Tarefas que executam código no pool de threads por padrão. Entretanto, Tarefas são mais gerais e podem representar operações assíncronas arbitrárias — mesmo aquelas que correspondem a, digamos, comunicação com um servidor ou leitura de dados no disco.

TaskCompletionSource é o mecanismo geral para criar Tarefas que representem operações assíncronas. TaskCompletionSource está associado a exatamente uma tarefa. Uma vez que o método SetResult é chamado em TaskCompletionSource, a Tarefa associada é concluída, retornando o valor resultante da Tarefa (veja a Figura 4).

Figura 4 Usando TaskCompletionSource

static void Main() {
  // Construct a TaskCompletionSource and get its 
  // associated Task
  TaskCompletionSource<int> tcs = 
    new TaskCompletionSource<int>();
  Task<int> task = tcs.Task;

  // Asynchronously, call SetResult on TaskCompletionSource
  ThreadPool.QueueUserWorkItem( _ => {
    Thread.Sleep(1000); // Do something
    tcs.SetResult(123);
  });

  Console.WriteLine(
    "The operation is executing asynchronously...");
  task.Wait();

  // And get the result that was placed into the task by 
  // the TaskCompletionSource
  Console.WriteLine("The task computed: {0}", task.Result);
}

Aqui eu uso um thread do pool de threads para chamar SetResult em TaskCompletionSource. Entretanto, um ponto importante a ser observado é que o método SetResult poderia ser chamado por qualquer código que tenha acesso a TaskCompletionSource — um manipulador de eventos para um evento Button.Click, uma Tarefa que concluiu algum cálculo, um evento gerado porque o servidor respondeu a uma solicitação e assim por diante.

Por isso, TaskCompletionSource é um mecanismo muito genérico para implementar operações assíncronas.

Convertendo um padrão IAsyncResult

Para usar Tarefas para a programação assíncrona, é importante poder interoperar com operações assíncronas expostas usando os modelos mais antigos. Embora TaskCompletionSource possa encapsular qualquer operação assíncrona e a expor como Tarefa, a API de Tarefa oferece um mecanismo prático para converter um padrão IAsyncResult em uma Tarefa: o método FromAsync.

Este exemplo usa o método FromAsync para converter a operação assíncrona baseada em IAsync­Result Dns.BeginGetHost­ Addresses em uma Tarefa:

static void Main() {
  Task<IPAddress[]> task = 
    Task<IPAddress[]>.Factory.FromAsync(
      Dns.BeginGetHostAddresses, 
      Dns.EndGetHostAddresses,
      "http://www.microsoft.com", null);
  ...
}

FromAsync facilita a conversão de operações assíncronas IAsyncResult em tarefas. De forma oculta, FromAsync é implementado de maneira semelhante ao exemplo de TaskCompletionSource utilizando ThreadPool. Eis uma aproximação simples de como ele é implementado, neste caso visando diretamente GetHostAddresses:

static Task<IPAddress[]> GetHostAddressesAsTask(
  string hostNameOrAddress) {

  var tcs = new TaskCompletionSource<IPAddress[]>();
  Dns.BeginGetHostAddresses(hostNameOrAddress, iar => {
    try { 
      tcs.SetResult(Dns.EndGetHostAddresses(iar)); }
    catch(Exception exc) { tcs.SetException(exc); }
  }, null);
  return tcs.Task;
}

Convertendo um padrão baseado em evento

As operações assíncronas baseadas em evento também podem ser convertidas em Tarefas usando a classe TaskCompletionSource. A classe Task não oferece um mecanismo interno para essa conversão — um mecanismo geral não é pratico porque o padrão assíncrono baseado em evento é apenas uma convenção.

Veja como converter uma operação assíncrona baseada em evento em uma tarefa. O exemplo de código mostra um método que usa um Uri e retorna uma Tarefa que representa a operação assíncrona WebClient.DownloadStringAsync:

static Task<string> DownloadStringAsTask(Uri address) {
  TaskCompletionSource<string> tcs = 
    new TaskCompletionSource<string>();
  WebClient client = new WebClient();
  client.DownloadStringCompleted += (sender, args) => {
    if (args.Error != null) tcs.SetException(args.Error);
    else if (args.Cancelled) tcs.SetCanceled();
    else tcs.SetResult(args.Result);
  };
  client.DownloadStringAsync(address);
  return tcs.Task;
}

Usando esse padrão e o padrão da seção anterior, é possível converter em Tarefa qualquer padrão assíncrono existente (seja baseado em evento ou em IAsyncResult).

Manipulando e compondo tarefas

Então por que usar Tarefas para representar operações assíncronas? O principal motivo é que Tarefas expõem métodos para manipular e compor operações assíncronas de maneira prática. Ao contrário das abordagens baseadas em IAsyncResult e eventos, uma Tarefa fornece um único objeto que mantém todas as informações relevantes sobre a operação assíncrona, como se unir a ela, como recuperar seus resultados, e assim por diante.

Uma coisa útil que você pode fazer com uma Tarefa é aguardar até que ela seja concluída. Você pode aguardar uma Tarefa, esperar até que todas as Tarefas de um conjunto sejam concluídas ou aguardar a conclusão de qualquer Tarefa do conjunto.

static void Main() {
  Task<int> task1 = new Task<int>(() => ComputeSomething(0));
  Task<int> task2 = new Task<int>(() => ComputeSomething(1));
  Task<int> task3 = new Task<int>(() => ComputeSomething(2));

  task1.Wait();
  Console.WriteLine("Task 1 is definitely done.");

  Task.WaitAny(task2, task3);
  Console.WriteLine("Task 2 or task 3 is also done.");

  Task.WaitAll(task1, task2, task3);
  Console.WriteLine("All tasks are done.");
}

Outro recurso útil de Tarefas é a capacidade de programar continuações: Tarefas que são executadas assim que outra Tarefa é concluída. Assim como a espera, é possível programar continuações que serão executadas quando uma Tarefa específica for concluída, quando todas as Tarefas de um conjunto forem concluídas ou quando qualquer Tarefa de um conjunto for concluída.

Este exemplo cria uma tarefa que consultará o DNS do endereço www.microsoft.com. Uma vez concluída a tarefa, a tarefa de continuação será iniciada e imprimirá o resultado no console:

static void Main() {
  Task<IPAddress[]> task = 
    Task<IPAddress[]>.Factory.FromAsync(
      Dns.BeginGetHostAddresses, 
      Dns.EndGetHostAddresses,
      "www.microsoft.com", null);

  task.ContinueWith(t => Console.WriteLine(t.Result));
  Console.ReadKey();
}

Vejamos exemplos mais interessantes que mostram a força da tarefa como representação de uma operação assíncrona. A Figura 5 mostra um exemplo que executa duas pesquisas de DNS em paralelo. Quando as operações assíncronas são representadas como tarefas, é fácil aguardar até que várias operações sejam concluídas.

Figura 5 Executando operações em paralelo

static void Main() {
  string[] urls = new[] { "www.microsoft.com", "www.msdn.com" };
  Task<IPAddress[]>[] tasks = new Task<IPAddress[]>[urls.Length];

  for(int i=0; i<urls.Length; i++) {
    tasks[i] = Task<IPAddress[]>.Factory.FromAsync(
      Dns.BeginGetHostAddresses,
      Dns.EndGetHostAddresses,
      urls[i], null);
  }

  Task.WaitAll(tasks);

  Console.WriteLine(
    "microsoft.com resolves to {0} IP addresses. msdn.com resolves to {1}",
    tasks[0].Result.Length,
    tasks[1].Result.Length);
}

A

  1. A
  2. Processe as páginas HTML
  3. Agregue as informações das páginas HTML

A Figura 6 ilustra como esse cálculo seria implementado, aproveitando a vantagem do método DownloadStringAsTask mostrado anteriormente neste artigo. Uma vantagem notável desta implementação é que os dois métodos CountParagraphs diferentes são executados em threads diferentes. Considerando a prevalência atual de máquinas com vários núcleos, um programa que distribuir seu intenso trabalho de cálculos entre vários threads terá uma vantagem de desempenho.

Figura 6 Baixando cadeias de caracteres de maneira assíncrona

static void Main() {
  Task<string> page1Task = DownloadStringAsTask(
    new Uri("http://www.microsoft.com"));
  Task<string> page2Task = DownloadStringAsTask(
    new Uri("http://www.msdn.com"));

  Task<int> count1Task = 
    page1Task.ContinueWith(t => CountParagraphs(t.Result));
  Task<int> count2Task = 
    page2Task.ContinueWith(t => CountParagraphs(t.Result));

  Task.Factory.ContinueWhenAll(
    new[] { count1Task, count2Task },
    tasks => {
      Console.WriteLine(
        "<P> tags on microsoft.com: {0}", 
        count1Task.Result);
      Console.WriteLine(
        "<P> tags on msdn.com: {0}", 
        count2Task.Result);
  });
        
  Console.ReadKey();
}

Executando tarefas em um contexto de sincronização

Às vezes é útil poder programar uma continuação que será executada em um determinado contexto de sincronização. Por exemplo, em aplicativos com uma interface do usuário, muitas vezes é útil poder programar uma continuação que será executada no thread de interface do usuário.

A maneira mais fácil de fazer com que uma Tarefa interaja com um contexto de sincronização é criar uma TaskScheduler que capture o contexto do thread atual. Para obter uma TaskScheduler para o thread de interface do usuário, chame o método estático FromCurrentSynchronizationContext no tipo TaskScheduler durante a execução no thread de interface do usuário.

Este exemplo baixa a página Web de www.microsoft.com e atribui o HTML baixado para a propriedade Text de uma caixa de texto do WPF:

void Button_Click(object sender, RoutedEventArgs e) {
  TaskScheduler uiTaskScheduler =
    TaskScheduler.FromCurrentSynchronizationContext()

  DownloadStringAsTask(new Uri("http://www.microsoft.com"))
    .ContinueWith(
       t => { textBox1.Text = t.Result; },
       uiTaskScheduler);
}

O corpo do método Button_Click configurará o cálculo assíncrono que atualiza a interface do usuário, mas Button_Click não aguarda a conclusão do cálculo. Dessa forma, o thread de interface do usuário não será bloqueado e poderá continuar atualizando a interface e respondendo a ações do usuário.

Como já mencionado, antes do .NET Framework 4, as operações assíncronas normalmente eram expostas usando o padrão IAsyncResult ou o padrão baseado em evento. Com o .NET Framework 4, agora é possível implantar a classe Task como outra representação útil de operações assíncronas. Quando representadas como tarefas, as operações assíncronas muitas vezes são mais fáceis de manipular e compor. Mais exemplos de como usar tarefas para programação assíncrona estão disponíveis nas amostras de ParallelExtensionsExtras, que podem ser baixadas de code.msdn.microsoft.com/ParExtSamples.

Igor Ostrovsky é engenheiro de desenvolvimento de software da equipe da plataforma de computação paralela da Microsoft. Ostrovsky registra suas aventuras de programação em igoro.com e colabora com a Programação Paralela no blog do .NET, em blogs.msdn.com/pfxteam.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Equipe do Concurrency Runtime