這個做法介紹了 C# 中 Task 提供了 IProgress 與 CancellationToken 可以用來實做進度條與取消任務等功能。
雖然我們可以幫非同步 API 新增取消的機制,但並不是每一種功能底層機制都支援取消功能,所以不應該把幫每個非同步 API 都加上能夠取消的多載版本。 同樣的問題在新增回報進度功能也一樣,你的非同步工作應該要有可以劃分階段並提供進度資訊的設計,否則不應該加上能夠回報進度的多載版本。
假設你寫了一個計算薪資的 API 內部包含了五個步驟,你就可以在每個步驟都額外實做回報進度的多載版本,也可以在第一二三步驟加上取消的多載版本, 確保在實際支付前都還能取消。
- 呼叫一個服務取得員工清單與工作時數。
- 呼叫第二個服務計算薪資所得稅金。
- 呼叫第三個服務產生薪資單並郵寄給員工。
- 呼叫第四的服務執行轉帳。
- 結束薪資結算期。
首先新增一個沒有支援取消與回報的版本。
public async Task RunPayroll(DateTime payrollPeriod)
{
// Step 1: Calculate hours and pay
var payrollData = await RetieveEmployeePayrollDataFor(payrollPeriod);
// Step 2: Calculate tax
var taxReporting = new Dictionary<EmployeePayrollData, TaxWithholding>();
foreach (var employee in payrollData)
{
var taxWitholding = await RetrieveTaxData(employee);
taxReporting.Add(employee, taxWitholding);
}
// Step 3: generate and email paystub documents
var paystubs = new List<Task>();
foreach (var payrollItem in taxReporting)
{
var payrollTask = GeneratePayrollDocument(payrollItem.Key, payrollItem.Value);
var emailTask = payrollTask.ContinueWith(
paystub => EmailPaystub(payrollItem.Key.Email, paystub.Result)
);
paystubs.Add(emailTask);
}
await Task.WhenAll(paystubs);
// Step 4: Deposit pay
var depositTasks = new List<Task>();
foreach (var payrollItem in taxReporting)
{
depositTasks.Add(MakeDeposit(payrollItem.Key,
payrollItem.Value));
}
await Task.WhenAll(depositTasks);
// Step 5: Close payroll period
await ClosePayrollPeriod(payrollPeriod);
}
接下來加上回報進度的功能。
public async Task RunPayroll(DateTime payrollPeriod, IProgress<(int, string)> progress)
{
progress?.Report((0, "Starting Payroll"));
// Step 1: Calculate hours and pay
var payrollData = await RetieveEmployeePayrollDataFor(payrollPeriod);
progress?.Report((20, "Retrieved employees and hours"));
// Step 2: Calculate tax
var taxReporting = new Dictionary<EmployeePayrollData, TaxWithholding>();
foreach (var employee in payrollData)
{
var taxWitholding = await RetrieveTaxData(employee);
taxReporting.Add(employee, taxWitholding);
}
progress?.Report((40, "Calculated Witholding"));
// Step 3: generate and email paystub documents
var paystubs = new List<Task>();
foreach (var payrollItem in taxReporting)
{
var payrollTask = GeneratePayrollDocument(
payrollItem.Key,
payrollItem.Value
);
var emailTask = payrollTask.ContinueWith(
paystub => EmailPaystub(payrollItem.Key.Email,
paystub.Result)
);
paystubs.Add(emailTask);
}
await Task.WhenAll(paystubs);
progress?.Report((60, "Emailed Paystubs"));
// Step 4: Deposit pay
var depositTasks = new List<Task>();
foreach (var payrollItem in taxReporting)
depositTasks.Add(MakeDeposit(payrollItem.Key,payrollItem.Value));
await Task.WhenAll(depositTasks);
progress?.Report((80, "Deposited pay"));
// Step 5: Close payroll period
await ClosePayrollPeriod(payrollPeriod);
progress?.Report((100, "complete"));
}
使用者可以這樣呼叫我們的方法,主要就是透過 IProgress
介面提供的 Report
來做到回報的功能。
void Main()
{
await generator.RunPayroll(DateTime.Now, new ProgressReporter());
}
public class ProgressReporter : IProgress<(int percent, string message)>
{
public void Report((int percent, string message) value)
{
Console.WriteLine($"{value.percent} completed: {value.message}";
}
}
接下來加上支援取消的多載版本。
public async Task RunPayroll(DateTime payrollPeriod, CancellationToken cancellationToken)
{
// Step 1: Calculate hours and pay
var payrollData = await RetieveEmployeePayrollDataFor(payrollPeriod);
cancellationToken.ThrowIfCancellationRequested();
// Step 2: Calculate tax
var taxReporting = new Dictionary<EmployeePayrollData, TaxWithholding>();
foreach (var employee in payrollData)
{
var taxWitholding = await RetrieveTaxData(employee);
taxReporting.Add(employee, taxWitholding);
}
cancellationToken.ThrowIfCancellationRequested();
// Step 3: generate and email paystub documents
var paystubs = new List<Task>();
foreach (var payrollItem in taxReporting)
{
var payrollTask = GeneratePayrollDocument(
payrollItem.Key, payrollItem.Value);
var emailTask = payrollTask.ContinueWith(
paystub => EmailPaystub(payrollItem.Key.Email,
paystub.Result));
paystubs.Add(emailTask);
}
await Task.WhenAll(paystubs);
cancellationToken.ThrowIfCancellationRequested();
// Step 4: Deposit pay
var depositTasks = new List<Task>();
foreach (var payrollItem in taxReporting)
{
depositTasks.Add(MakeDeposit(payrollItem.Key,
payrollItem.Value));
}
await Task.WhenAll(depositTasks);
// Step 5: Close payroll period
await ClosePayrollPeriod(payrollPeriod);
}
呼叫者可以透過 CancellationTokenSource
管理 Token 的生命週期,注意到這裡是透過拋出 TaskCanceledException
來達到中止任務的效果。
void Main()
{
var cts = new CancellationTokenSource();
generator.RunPayroll(DateTime.Now, cts.Token);
// to cancel:
cts.Cancel();
}
最後將所有內容合併到一起讓使用者選擇需要的版本,這裡把共用的程式碼都放到一個方法中,當 progress
為 null 是不會有錯誤的也不會回報進度,
cancelationToken
也一樣只會新增但實際上沒有人管理取消功能所以不會被要求取消。
public Task RunPayroll(DateTime payrollPeriod) => RunPayroll(payrollPeriod, new CancellationToken(), null);
public Task RunPayroll(DateTime payrollPeriod, CancellationToken cancellationToken) =>
RunPayroll(payrollPeriod, cancellationToken, null);
public Task RunPayroll(DateTime payrollPeriod, IProgress<(int, string)> progress) =>
RunPayroll(payrollPeriod, new CancellationToken(), progress);
public async Task RunPayroll(DateTime payrollPeriod, CancellationToken cancelationToken, IProgress<(int, string)> progress)
{
progress?.Report((0, "Starting Payroll"));
// Step 1: Calculate hours and pay
var payrollData = await RetieveEmployeePayrollDataFor(payrollPeriod);
cancelationToken.ThrowIfCancellationRequested();
progress?.Report((20, "Retrieved employees and hours"));
// Step 2: Calculate tax
var taxReporting = new Dictionary<EmployeePayrollData, TaxWithholding>();
foreach (var employee in payrollData)
{
var taxWitholding = await RetrieveTaxData(employee);
taxReporting.Add(employee, taxWitholding);
}
cancelationToken.ThrowIfCancellationRequested();
progress?.Report((40, "Calculated Witholding"));
// Step 3: generate and email paystub documents
var paystubs = new List<Task>();
foreach (var payrollItem in taxReporting)
{
var payrollTask = GeneratePayrollDocument(
payrollItem.Key, payrollItem.Value);
var emailTask = payrollTask.ContinueWith(
paystub => EmailPaystub(payrollItem.Key.Email,
paystub.Result));
paystubs.Add(emailTask);
}
await Task.WhenAll(paystubs);
cancelationToken.ThrowIfCancellationRequested();
progress?.Report((60, "Emailed Paystubs"));
// Step 4: Deposit pay
var depositTasks = new List<Task>();
foreach (var payrollItem in taxReporting)
{
depositTasks.Add(MakeDeposit(payrollItem.Key,
payrollItem.Value));
}
await Task.WhenAll(depositTasks);
progress?.Report((80, "Deposited pay"));
// Step 5: Close payroll period
await ClosePayrollPeriod(payrollPeriod);
cancelationToken.ThrowIfCancellationRequested();
progress?.Report((100, "complete"));
}
Summary
這個做法介紹 Task 能夠支援任務取消與回報進度的功能,可以按照自己的需求評估是否需要實做。