C# -  Concorrência , Paralelismo, Multithread e Assincronismo no .NET - II


Hoje vamos continuar a tratar dos conceitos de concorrência, paralelismo, multithreading e assincronismo aplicados na plataforma .NET focando a programação assíncrona.

Continuando a primeira parte do artigo onde apresentamos os conceitos sobre concorrência, paralelismo, multithread e assincronismo vamos tratar com a programação assíncrona.

A programação assíncrona

Vamos iniciar analisando um trecho de código usando na programação assíncrona na linguagem C#.

using System;
using System.Threading.Tasks;
namespace C_Async1
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Chamando DemoAsync()...");
            await DemoAsync();
            Console.WriteLine("Após chamar DemoAsync...");
            Console.ReadLine();
        }
        static async Task DemoAsync()
        {
            int val = 15;
            // aguarda 1 segundo - assíncro
            await Task.Delay(TimeSpan.FromSeconds(1));
            val *= 2;
            // aguarda 1 segundo - assíncrono
            await Task.Delay(TimeSpan.FromSeconds(1));
            Console.WriteLine(val);
        }
    }
}

Na linguagem C# a programação assíncrona usa as palavras chave async e await.

Um método assíncrono começa a ser executado de forma síncrona, como qualquer outro método.

Dentro de um método assíncrono, a palavra-chave await executa uma espera assíncrona em seu argumento :

  1. Primeiro, ele verifica se a operação já foi concluída; se for, ele continua executando (de forma síncrona). Caso contrário, ele pausará o método assíncrono e retornará uma tarefa incompleta;
     
  2. Quando essa operação for concluída algum tempo depois, o método assíncrono continuará a execução;

Você pode pensar em um método assíncrono como tendo várias partes síncronas, divididas por instruções await.

A primeira parte síncrona é executada em qualquer que seja a thread que chama o método, mas onde as outras partes síncronas são executadas ?

A resposta é um pouco complicada.

Quando você espera uma tarefa (o cenário mais comum), um contexto é capturado quando o await decide pausar o método. Este contexto é o SynchronizationContext atual, a menos que seja nulo, caso em que o contexto é o TaskScheduler atual.

O método retoma a execução dentro desse contexto capturado. Normalmente, esse contexto é o contexto da IU (se você estiver no thread de IU), um contexto de solicitação ASP.NET (se você estiver processando uma solicitação ASP.NET) ou o contexto do pool de threads (a maioria das outras situações).

Portanto, no código anterior, todas as partes síncronas tentarão retomar no contexto original. Se você chamar o método DemoAsync a partir de um thread de IU, cada uma de suas partes síncronas será executada nessa thread de IU; mas se você chamá-lo de um thread do pool de threads, cada uma de suas partes síncronas será executada em um thread do pool de threads.

Você pode evitar esse comportamento padrão aguardando o resultado do método de extensão ConfigureAwait e passando false para o parâmetro continueOnCapturedContext.

O código a seguir do método DemoAsync2 começará na thread de chamada e, depois de ser pausado por um await, ele continuará em uma thread do pool de threads:

 async Task DemoAsync2()
 {
     int val = 15;
     // aguarda 1 segundo - assíncro
     await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
     val *= 2;
     // aguarda 1 segundo - assíncro
     await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
    Console.WriteLine(val.ToString());
 }

Dessa forma a palavra-chave await não se limita a trabalhar com um task; pode funcionar com qualquer tipo de espera que siga um certo padrão.

Como exemplo, a API do Windows Runtime define suas próprias interfaces para operações assíncronas. Eles não são conversíveis em Task, mas seguem o padrão esperado, portanto, você pode aguardá-los diretamente.

Esses aguardáveis são mais comuns em aplicativos da Windows Store, mas na maioria das vezes, aguardar exigirá uma usar um Task ou um Task<T>.

Existem duas maneiras básicas de criar uma instância de Task. Algumas tarefas representam o código real que uma CPU deve executar; essas tarefas computacionais devem ser criadas chamando Task.Run (ou TaskFactory.StartNew se você precisar que elas sejam executadas em um agendador específico).

Outras tarefas representam uma notificação; essas tarefas baseadas em eventos são criadas por TaskCompletionSource<T> (ou um de seus atalhos). No entanto, a maioria das tarefas de E/S usa TaskCompletionSource<T>.

E estamos conversados...

Aquele que crê no Filho (Jesus) tem a vida eterna; mas aquele que não crê no Filho não verá a vida, mas a ira de Deus sobre ele permanece.
João 3:36

Referências:


José Carlos Macoratti