Fenner
A linguagem

async/await e Fenner.Tasks

Funções async que já correm no pool de threads, await, laço paralelo, cancelamento cooperativo e as peças de sincronização.

O que é e por que existe#

Trabalho que pode correr em paralelo (calcular, esperar rede, ler disco) não deveria obrigar a escrever threads à mão. Na linguagem, uma função marcada async já sai correndo no pool de threads e devolve uma TTask<T>; o await espera o resultado (e relança a exceção, se a tarefa falhou). A biblioteca Fenner.Tasks completa com cancelamento, trava, evento, semáforo, WaitAll, WhenAny, Delay e um laço paralelo.

A página de Lambdas, eventos e async apresenta o async básico. Aqui está o conjunto completo.

Na linguagem#

FormaO que faz
async function F: Tdevolve TTask<T>; começa a correr na hora ("hot start")
async procedure Pdevolve TTaskNoReturn
await Eespera a tarefa e devolve o valor (ou relança a exceção dela)
R0, R1 := await T0, T1espera em bloco

A TTask é da RTL. Toda tarefa criada deve receber Free depois de concluída.

Fenner.Tasks (Compiler\extras)#

O módulo mora fora da RTL; o .config do projeto coloca Compiler\extras no SourcePath (o exemplo abaixo mostra o arquivo).

ItemO que faz
TCancellationTokenCancel, IsCancelled, ThrowIfCancelled (levanta TOperationCanceledException), WaitCancelled(ms)
TLockseção crítica reentrante: Enter, Leave, TryEnter
TEventSetEvent, ResetEvent, Wait(ms): Bool8
TCountingSemaphoreAcquire(ms): Bool8, Release(n) (o nome TSemaphore já é da RTL)
TTasks.Run(proc)dispara no pool
TTasks.WaitAll([…]), WhenAll([…])esperam todas; relançam a exceção da primeira na ordem do vetor
TTasks.WaitAny([…], ms), WhenAny([…])índice da primeira concluída
TTasks.Delay(ms, token)tarefa que dorme; acorda no Cancel com TOperationCanceledException
TTasks.ContinueWith(task, proc)roda proc(task) depois do fim da tarefa
TParallel.ForRange(de, ate, proc, token)laço paralelo por faixas contíguas (uma por CPU)
Async.fxp
program Async;
// Demo: `async`/`await` da linguagem e a biblioteca Fenner.Tasks (Compiler\extras).
// Uma função `async` devolve uma TTask<T> que já corre no pool de threads; `await` espera o resultado.

uses System.Remainder, Fenner.Tasks;   // o Async.config põe Compiler\extras no SourcePath

type
  TService = class(TObject)
  public
    async function Square(const AValue: Int32): Int32;
    async function Fail: Int32;
  end class;

async function TService.Square(const AValue: Int32): Int32;
begin
  Exit(AValue * AValue);
end function;

async function TService.Fail: Int32;
begin
  raise Exception.Create('falha dentro da task');
end function;

var
  LService: TService;
  LTaskA, LTaskB, LTaskFail: TTask<Int32>;
  LToken: TCancellationToken;
  LSlots: array[0..9] of Int32;
  LSum, I: Int32;
begin
  LService := TService.Create;

  // as duas chamadas já saem correndo em paralelo
  LTaskA := LService.Square(3);
  LTaskB := LService.Square(4);
  TTasks.WaitAll([LTaskA, LTaskB]);                 // espera as duas terminarem
  WriteLn('3*3 + 4*4 = ', await LTaskA + await LTaskB);
  LTaskA.Free;
  LTaskB.Free;

  // a exceção dentro da task só reaparece para quem faz await
  LTaskFail := LService.Fail;
  try
    WriteLn(await LTaskFail);
  except
    on E: Exception do WriteLn('capturada no await: ', E.Message);
  end try;
  LTaskFail.Free;

  // laço paralelo: cada índice grava só no SEU elemento do vetor, então não precisa de lock
  LToken := TCancellationToken.Create;
  TParallel.ForRange(0, 9, procedure(const K: Int32); begin LSlots[K] := K * K; end, LToken);
  LSum := 0;
  for I := 0 to 9 do
    LSum := LSum + LSlots[I];
  WriteLn('soma dos quadrados 0..9 = ', LSum);

  // cancelamento cooperativo: quem trabalha consulta o token
  LToken.Cancel;
  try
    LToken.ThrowIfCancelled;
  except
    on E: TOperationCanceledException do WriteLn('operação cancelada');
  end try;
  LToken.Free;
  LService.Free;
end.
Async.config
Platform MSWin64
Default Platform MSWin64
Mode Debug
Default Mode Debug
PlatformMode All * *
PlatformMode MSWin64Debug MSWin64 Debug
Config All SourcePath ($FcPath)\..\extras
Saída
3*3 + 4*4 = 25
capturada no await: falha dentro da task
soma dos quadrados 0..9 = 285
operação cancelada

Demo 46. Duas tarefas em paralelo, exceção que só aparece no await, ForRange e cancelamento.

Armadilhas#

  • Segurança de threads: TList, TDictionary, uma UString compartilhada e qualquer objeto da RTL sem garantia não são thread-safe. Use TLock, ou faça cada tarefa escrever só no seu elemento de um vetor (como no ForRange do exemplo). O ARC é atômico, mas isso protege só a contagem.
  • A exceção só aparece para quem faz await/Wait. Uma tarefa que falha e nunca é esperada falha em silêncio.
  • O await bloqueia a thread (não há corrotinas por baixo): uma tarefa que espera outra prende uma thread do pool, e esperas aninhadas demais esgotam o pool, que no Windows cresce devagar.
  • Cancelamento é cooperativo: quem trabalha precisa consultar o token.
  • WaitAny/WhenAny sondam a cada 1 ms. Não há ContinueWith que devolva valor nem Run<T>.
  • Só Windows (a camada de sistema da biblioteca usa Kernel32).
  • O laço se chama ForRange porque For é palavra reservada.

Veja também#

  • Iteradores e corrotinas, para gerar valores sob demanda.
  • Demo 46-async-tasks; demos 73-threads e 74-sincronizacao-eventos para threads "de baixo nível".