tips - async/await による非同期処理 その 4

 非同期処理で重たい処理をおこなう場合に、 中断したくなるときがあります。 ここでは async/await 演算子による非同期処理を途中でキャンセルする例を紹介します。

 サンプルプロジェクトはここからダウンロードできます。

ページ内リンク

概要

 まず UI の XAML コードとその実行結果を示します。

<Window x:Class="AsyncSample4.Views.MainView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainView" Height="300" Width="300">
    <StackPanel>
        <Button Content="Click me!" Command="{Binding ButtonCommand}" />
        <TextBlock Text="{Binding Result}" />
        <Button Content="Cancel" Command="{Binding CancelCommand}" />
    </StackPanel>
</Window>
Code 1 : サンプルアプリケーション外観の XAML コード
Fig.1 : サンプルアプリケーションの外観
ボタンを押したら非同期処理で重たい処理を実行し、 キャンセルボタンでその処理を中断させるサンプルです。

中断処理を含む非同期処理の記述

 非同期処理の記述は「async/await による非同期処理 その 1」で示したサンプルプログラムとほぼ同じですが、 一部だけ変更して中断処理を実現しようと思います。

 まず、ViewModel のプロパティです。

#region 公開プロパティ
private string result = "まだ実行してないよ。";
/// <summary>
/// 実行結果を取得または設定します。
/// </summary>
public string Result
{
    get { return result; }
    set { SetProperty(ref result, value); }
}

private bool isBusy;
/// <summary>
/// ビジー状態かどうかを取得または設定します。
/// </summary>
public bool IsBusy
{
    get { return isBusy; }
    set
    {
        if (SetProperty(ref isBusy, value))
            ButtonCommand.RaiseCanExecuteChanged();
    }
}

private DelegateCommand buttonCommand;
/// <summary>
/// ボタンコマンドを取得します。
/// </summary>
public DelegateCommand ButtonCommand
{
    get
    {
        return buttonCommand ?? (buttonCommand = new DelegateCommand(
            _ =>
            {
                System.Console.WriteLine("Thread[{0}] 非同期処理をコールします。", Thread.CurrentThread.ManagedThreadId);
                HeavyWorkAsync();
                System.Console.WriteLine("Thread[{0}] 非同期処理をコールしました。", Thread.CurrentThread.ManagedThreadId);
            },
            _ =>
            {
                return !IsBusy;
            }));
    }
}

private DelegateCommand cancelCommand;
/// <summary>
/// キャンセルコマンドを取得します。
/// </summary>
public DelegateCommand CancelCommand
{
    get
    {
        return cancelCommand ?? (cancelCommand = new DelegateCommand(
        _ =>
        {
            CancelTokenSource.Cancel();
        },
        _ =>
        {
            return CancelTokenSource != null;
        }));
    }
}
#endregion 公開プロパティ

#region private プロパティ
private CancellationTokenSource cancelTokenSource;
/// <summary>
/// キャンセル用のトークン
/// </summary>
private CancellationTokenSource CancelTokenSource
{
    get { return cancelTokenSource; }
    set
    {
        cancelTokenSource = value;
        CancelCommand.RaiseCanExecuteChanged();
    }
}
#endregion private プロパティ
Code 2 : ViewModel の公開プロパティ
Result プロパティで実行中もしくは実行終了を表し、 IsBusy プロパティで実行中を表現するところは同じです。 また、ButtonCommand プロパティによって非同期処理を開始するところも同じです。 最後に CancelCommand プロパティが追加されており、 private プロパティである CancellationTokenSource クラスの CancelTokenSource プロパティを使用しています。

 非同期処理を途中で中断するには、CancellationTokenSource クラスを使用します。 非同期処理のほうで CancellationTokenSource クラスの持つ IsCancellationRequested プロパティを確認し、 これが true のときに処理を中断するようにします。 中断を指令する側は、上記のように Cancel() メソッドをコールするだけとなります。

 非同期処理を開始する HeavyWorkAsync() メソッドを含む非同期関連のメソッドは次のようになります。

#region private メソッド
/// <summary>
/// 重たい処理を非同期で実行します。
/// </summary>
private async void HeavyWorkAsync()
{
    IsBusy = true;
    CancelTokenSource = new CancellationTokenSource();
    Result = "只今実行中...";
    System.Console.WriteLine("Thread[{0}] 非同期処理を実行します。", Thread.CurrentThread.ManagedThreadId);
    await HeavyWorkTask();
    // ここで一旦 return される

    // 非同期処理が終了したらここから再開する
    System.Console.WriteLine("Thread[{0}] 非同期処理を実行しました。", Thread.CurrentThread.ManagedThreadId);
    if (CancelTokenSource.IsCancellationRequested)
    {
        Result = "キャンセルしました。";
    }
    else
    {
        Result = "終了しました。";
    }
    CancelTokenSource = null;
    IsBusy = false;
}

/// <summary>
/// 重たい処理を実行するタスクを返します。
/// </summary>
/// <returns>重たい処理を実行するタスク</returns>
private Task HeavyWorkTask()
{
    return Task.Run(() =>
    {
        // 重たい処理を実行
        System.Console.WriteLine("Thread[{0}] 重たい処理を実行します。", Thread.CurrentThread.ManagedThreadId);
        HeavyWork(CancelTokenSource != null ? CancelTokenSource.Token : new CancellationToken(false));
        System.Console.WriteLine("Thread[{0}] 重たい処理を終了します。", Thread.CurrentThread.ManagedThreadId);
    });
}

/// <summary>
/// 重たい処理を実行します。
/// </summary>
/// <param name="token">キャンセル処理をおこなうための <code>System.Threading.CancellationToken</code> クラスを指定します。</param>
private void HeavyWork(CancellationToken token)
{
    int count = 0;
    while (count++ < 100)
    {
        // キャンセルされたかどうか確認する
        if (token.IsCancellationRequested)
        {
            System.Console.WriteLine("Thread[{0}] 重たい処理を中断します。", Thread.CurrentThread.ManagedThreadId);
            return;
        }

        // 重たい処理
        Thread.Sleep(30);
    }
}
#endregion private メソッド
Code 3 : 非同期処理関連のメソッド
HeavyWorkAsync() メソッドから重たい処理を実行するタスクを返す HeavyWorkTask() メソッドを await 付きでコールします。 HeavyWorkTask() メソッドの中では、重たい処理の実体である HeavyWork() メソッドをコールしますが、 CancellationToken 構造体を渡すようにしています。 HeavyWork() メソッドでは、本当にやりたい処理を始める前に IsCancellationRequested プロパティを確認し、 キャンセルされていなければ処理を実行するというようになっています。

 実行結果を次の図に示します。

Fig.2 : 実行中にキャンセルボタンが有効になる
Fig.3 : キャンセルボタンを押すと中断される

Designed by CSS.Design Sample