第二十章:非同步和檔案I/O.(十九)
取消作業
到目前為止顯示的兩個Mandelbrot程式僅用於生成單個影象,因此一旦啟動它就不可能取消該作業。但是,在一般情況下,您需要為使用者提供一種便利,以擺脫冗長的後臺作業。
儘管您可以將自己的一個取消系統放在一起,但System.Threading名稱空間已經為您提供了一個名為CancellationTokenSource的類和一個名為CancellationToken的結構。
以下是它的工作原理:
程式建立一個CancellationTokenSource以用於特定的非同步方法。 CancellationTokenSource類定義名為Token的屬性,該屬性返回CancellationToken。此CancellationToken值將傳遞給非同步方法。非同步方法定期呼叫CancellationToken的IsCancellationRequested方法。此方法通常返回false。
當程式想要取消非同步操作時(可能是響應某些使用者輸入),它呼叫CancellationTokenSource的Cancel方法。下次非同步方法呼叫CancellationToken的IsCancellationRequested方法時,該方法返回true,因為已請求取消。非同步方法可以選擇如何
停止執行,也許是一個簡單的return語句。
然而,通常採用不同的方法。非同步方法可以簡單地呼叫ThrowIfCancellationRequested方法,而不是呼叫CancellationToken的IsCancellationRequested方法。如果已請求取消,則非同步方法將通過引發OperationCanceledException停止執行。
這意味著await運算子必須是try塊的一部分,但正如您所見,這通常是處理檔案時的情況,因此它不會新增太多額外的程式碼,並且程式可以簡單地處理取消另一種形式的例外。
MandelbrotCancellation程式演示了這種技術。 XAML檔案現在有第二個按鈕,標記為“取消”,最初被禁用:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MandelbrotCancellation.MandelbrotCancellationPage"> <ContentPage.Padding> <OnPlatform x:TypeArguments="Thickness" iOS="0, 20, 0, 0" /> </ContentPage.Padding> <StackLayout> <Grid VerticalOptions="FillAndExpand"> <ContentView Padding="10, 0" VerticalOptions="Center"> <ProgressBar x:Name="progressBar" /> </ContentView> <Image x:Name="image" /> </Grid> <Grid> <Button x:Name="calculateButton" Grid.Column="0" Text="Calculate" FontSize="Large" HorizontalOptions="Center" Clicked="OnCalculateButtonClicked" /> <Button x:Name="cancelButton" Grid.Column="1" Text="Cancel" FontSize="Large" IsEnabled="False" HorizontalOptions="Center" Clicked="OnCancelButtonClicked" /> </Grid> </StackLayout> </ContentPage>
程式碼隱藏檔案現在有一個更廣泛的OnCalculateButtonClicked方法。 首先禁用“計算”按鈕並啟用“取消”按鈕。 它建立一個新的Cancellation TokenSource物件,並將Token屬性傳遞給CalculateMandelbrotAsync。 OnCancelButtonClicked方法負責在CancellationTokenSource物件上呼叫Cancel。 CalculateMandelbrotAsync方法以與報告進度相同的速率呼叫ThrowIfCancellationRequested方法。 OnCalculateButtonClicked方法捕獲異常,該方法通過重新啟用“計算”按鈕進行另一次嘗試來響應:
public partial class MandelbrotCancellationPage : ContentPage
{
static readonly Complex center = new Complex(-0.75, 0);
static readonly Size size = new Size(2.5, 2.5);
const int pixelWidth = 1000;
const int pixelHeight = 1000;
const int iterations = 100;
Progress<double> progressReporter;
CancellationTokenSource cancelTokenSource;
public MandelbrotCancellationPage()
{
InitializeComponent();
progressReporter = new Progress<double>((double value) =>
{
progressBar.Progress = value;
});
}
async void OnCalculateButtonClicked(object sender, EventArgs args)
{
// Configure the UI for a background process.
calculateButton.IsEnabled = false;
cancelButton.IsEnabled = true;
cancelTokenSource = new CancellationTokenSource();
try
{
// Render the Mandelbrot set on a bitmap.
BmpMaker bmpMaker = await CalculateMandelbrotAsync(progressReporter,
cancelTokenSource.Token);
image.Source = bmpMaker.Generate();
}
catch (OperationCanceledException)
{
calculateButton.IsEnabled = true;
progressBar.Progress = 0;
}
catch (Exception)
{
// Shouldn't occur in this case.
}
cancelButton.IsEnabled = false;
}
void OnCancelButtonClicked(object sender, EventArgs args)
{
cancelTokenSource.Cancel();
}
Task<BmpMaker> CalculateMandelbrotAsync(IProgress<double> progress,
CancellationToken cancelToken)
{
return Task.Run<BmpMaker>(() =>
{
BmpMaker bmpMaker = new BmpMaker(pixelWidth, pixelHeight);
for (int row = 0; row < pixelHeight; row++)
{
double y = center.Imaginary - size.Height / 2 + row * size.Height / pixelHeight;
// Report the progress.
progress.Report((double)row / pixelHeight);
// Possibly cancel.
cancelToken.ThrowIfCancellationRequested();
for (int col = 0; col < pixelWidth; col++)
{
double x = center.Real - size.Width / 2 + col * size.Width / pixelWidth;
Complex c = new Complex(x, y);
Complex z = 0;
int iteration = 0;
bool isMandelbrotSet = false;
if ((c - new Complex(-1, 0)).MagnitudeSquared < 1.0 / 16)
{
isMandelbrotSet = true;
}
// http://www.reenigne.org/blog/algorithm-for-mandelbrot-cardioid/
else if (c.MagnitudeSquared * (8 * c.MagnitudeSquared - 3) <
3.0 / 32 - c.Real)
{
isMandelbrotSet = true;
}
else
{
do
{
z = z * z + c;
iteration++;
}
while (iteration < iterations && z.MagnitudeSquared < 4);
isMandelbrotSet = iteration == iterations;
}
bmpMaker.SetPixel(row, col, isMandelbrotSet ? Color.Black : Color.White);
}
}
return bmpMaker;
}, cancelToken);
}
}
CancellationToken也作為第二個引數傳遞給Task.Run。 這不是必需的,但它允許Task.Run方法在已經請求取消甚至開始之前跳過大量工作。
另請注意,該程式碼現在跳過大型心形指標。 註釋引用一個網頁,該網頁會在您想要檢查數學的情況下派生公式。