Async-Await – A alegoria do restaurante

Introdução

Com a introdução de capacidades assíncronas nas linguagens de programação C# e Visual Basic todas as APIs assíncronas tendem a seguir este padrão.

No entanto, muitos programadores não entendem ainda como lidar com esta nova realidade.

Este artigo não pretende explicar como funciona esta funcionalidade, mas apresentar uma alegoria que permite formar um modelo mental de como se deve funcionar com esta tecnologia.

Aplicações cliente

A maioria das interfaces gráficas de utilizador (smartphones, PCs, tablets, etc.) é gerida por um thread dedicado à gestão da interação com o utilizador e atualização do ecrã. Como tal, deve-se evitar bloquear esse thread ou efetuar nele computações que não estejam relacionadas com a sua função.

Imaginemos um restaurante com apenas uma empregada de mesa encarregue do serviço à sala. Essa empregada de mesa só pode executar uma tarefa de cada vez e, se for bloqueada, não vai poder atender nenhum cliente.

Quando um novo cliente chega, notifica a empregada de mesa que chegou e esta, assim que possível, acompanha-o a uma mesa, entrega-lhe uma ementa e prossegue para a próxima tarefa enquanto o cliente faz a sua escolha.

Quando cliente tiver alguma dúvida, chama a empregada de mesa, esta desloca-se à mesa, esclarece a dúvida, e parte para a tarefa seguinte.

Quando o cliente souber o que vai pedir, chama a empregada de mesa, faz o seu pedido, a empregada de mesa entrega-o na cozinha e parte para a tarefa seguinte.

Quando o serviço da cozinha está pronto, a empregada de mesa leva-o ao cliente e parte para a tarefa seguinte.

Como se pode deduzir, para que a empregada de mesa consiga servir o maior número de clientes possível sem que estes esperem muito e tenham a sensação de que são o único cliente, não se deve chamar a empregada de mesa demasiadas vezes nem chamá-la quando estamos a fazer outra coisa (como ler a ementa, atender um telefonema, etc.). De igual modo, não devemos bloquear a empregada de mesa se quisermos falar com outro funcionário do restaurante (o cozinheiro, por exemplo).

Colocando esta interação em C#, seria algo como:

void HandleCustomer(object source, CustomerEventArgs e)
{
    WalkCustomerToTable();
    GiveMenuToCustomer();
    while (true)
    {
        switch (await CustomerRequestAsync())
        {
            case Question:
                var question =
                    await GetCustomerQuestionAsync();
                if (question.CanAnswer())
                {
                    AnswerCustomerQuestion(question);
                }
                else
                {
                    await Task.Run(() => 
                        RequestSomeoneElseToAnswer());
                }
                break;
            case Order:
                var order =
                    await GetCustomerOrderAsync();
                TakeCustomerOrder(e.Customer, order);
                break;
            case Bill:
                GiveBillToCustomer(e.Customer);
                break;
            case Payment:
                var payment =
                    await CustomerPaymentAsync();
                ProcessCustomerPayment(
                    e.Customer,
                    payment);
                return;
            ...
        }
    }
}

A interação da empregada de mesa com a cozinha pode ser representada de forma semelhante:

void HandleCustormerOrder(object sender, CustomerOrderEventArgs e)
{
    var meal = await GetMealFromOrderAsync(e.Order);
    DeliverMealToCustomer(e.Customer, meal);
}

Nesta analogia, a empregada de mesa é o thread de UI.

Aplicações servidor

O objetivo de um servidor (um servidor web, por exemplo) é servir o cliente do início ao fim o mais rapidamente possível para que o cliente vá à sua vida e possa ser servido outro cliente.

Isto é equivalente a restaurante de fast-food. Existe um conjunto (pool) de estações de atendimento que atende um cliente de cada vez do princípio ao fim.

Numa configuração destas, é mais eficiente esperar um pouco que o cliente pense do que fazê-lo sair da fila para pensar e depois voltar para a fila e recuperar o estado do seu pedido quando finalmente chegar a um empregado. Assim como também não faz sentido o cliente pedir umas coisas a um empregado e outras a outro – Task.Run.

Mas quando o pedido está completo, o empregado paraleliza ao máximo possível as tarefas a realizar, mais uma vez, com o objetivo de despachar o cliente o mais rapidamente possível. Uma vez que são devidamente organizados, estes empregados até podem trocar de cliente entre si sem que o cliente tenha uma perceção negativa do desempenho do “sistema”.

Neste caso, atender o cliente seria algo como:

Meal HandleCustomerOrder(CustomerRequest request)
{
    var order = GetOrderFromRequest(request);
    var kitchenTask =
        GetRequestFromKitchenAsync(order);
    var beverageTask = GetBeverageAsync(order);
    var fries = GetFries(order);

    await Task.WhenAll(kitchenTask, beverageTask);

    return new Meal(
        kitchenTask.Result,
        beverageTask.Result,
        fries);
}

Conclusão

Embora muito simplista, esta analogia permite equiparar mentalmente o que se passa na execução do código assíncrono com o mundo real e quais os padrões mais comuns.

Recursos

Publicado na edição 49 (PDF) da Revista PROGRAMAR.