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#
| Forma | O que faz |
|---|---|
async function F: T | devolve TTask<T>; começa a correr na hora ("hot start") |
async procedure P | devolve TTaskNoReturn |
await E | espera a tarefa e devolve o valor (ou relança a exceção dela) |
R0, R1 := await T0, T1 | espera 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).
| Item | O que faz |
|---|---|
TCancellationToken | Cancel, IsCancelled, ThrowIfCancelled (levanta TOperationCanceledException), WaitCancelled(ms) |
TLock | seção crítica reentrante: Enter, Leave, TryEnter |
TEvent | SetEvent, ResetEvent, Wait(ms): Bool8 |
TCountingSemaphore | Acquire(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) |
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.Platform MSWin64
Default Platform MSWin64
Mode Debug
Default Mode Debug
PlatformMode All * *
PlatformMode MSWin64Debug MSWin64 Debug
Config All SourcePath ($FcPath)\..\extras3*3 + 4*4 = 25
capturada no await: falha dentro da task
soma dos quadrados 0..9 = 285
operação canceladaDemo 46. Duas tarefas em paralelo, exceção que só aparece no await, ForRange e cancelamento.
Armadilhas#
- Segurança de threads:
TList,TDictionary, umaUStringcompartilhada e qualquer objeto da RTL sem garantia não são thread-safe. UseTLock, ou faça cada tarefa escrever só no seu elemento de um vetor (como noForRangedo 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
awaitbloqueia 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/WhenAnysondam a cada 1 ms. Não háContinueWithque devolva valor nemRun<T>.- Só Windows (a camada de sistema da biblioteca usa Kernel32).
- O laço se chama
ForRangeporqueForé palavra reservada.
Veja também#
- Iteradores e corrotinas, para gerar valores sob demanda.
- Demo
46-async-tasks; demos73-threadse74-sincronizacao-eventospara threads "de baixo nível".