C#のWinFormsでCancellationTokenSourceのサンプル

C# コンピュータ
C#
CancellationTokenSourceを使ってasync/await内でTaskをキャンセルする機能を試してみたいと思います。

プロジェクトの作成

dotnet new winforms -n プロジェクト名
cd プロジェクト名
code .

ソースコード

ファイル名:Forms1.cs

using System.Diagnostics;

namespace AsyncSample2;

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        var panel1 = new FlowLayoutPanel
        {
            Dock = DockStyle.Fill,
            Padding = new Padding(10),
        };
        var button1 = new Button
        {
            Size = new Size(100, 50),
            Enabled = true,
            Text = "実行",
        };
        var button2 = new Button
        {
            Size = new Size(100, 50),
            Enabled = false,
            Text = "キャンセル",
        };
        var progress1 = new ProgressBar
        {
            Size = new Size(200, 40),
        };
        var label1 = new Label()
        {
            Size = new Size(200,40),
            Text = $"{Thread.CurrentThread.ManagedThreadId}: ",
        };
        panel1.Controls.AddRange(new Control[]
        {
            button1,
            button2,
            progress1,
            label1,
        });
        panel1.SetFlowBreak(button2, true);
        panel1.SetFlowBreak(progress1, true);
        Controls.Add(panel1);
        IProgress<int> progress = new Progress<int>(x =>
        {
            label1.Text = $"{Thread.CurrentThread.ManagedThreadId}: Progress";
            progress1.Value = x;
        });
        
        CancellationTokenSource? cts = null;
        
        button1.Click += async (s, e) =>
        {
            label1.Text = $"{Thread.CurrentThread.ManagedThreadId}: Click Start";

            button1.Enabled = false;
            button2.Enabled = true;
            progress1.Value = 0;
            cts = new CancellationTokenSource();
            try
            {
                await Task.Run(()=>
                {
                    cts.Token.ThrowIfCancellationRequested();
                    for(int i = 0; i < 100; i++)
                    {
                        //label1.Text = $"{Thread.CurrentThread.ManagedThreadId}: Task.Run{i}";
                        progress.Report(i+1);
                        if (cts.IsCancellationRequested)
                            throw new OperationCanceledException("キャンセル");
                        Task.Delay(10).Wait();
                    }
                }, cts.Token);
                label1.Text = $"{Thread.CurrentThread.ManagedThreadId}: 完了";
            }
            catch (OperationCanceledException ex)
            {
                label1.Text = $"{Thread.CurrentThread.ManagedThreadId}: Catch {ex.Message}";
            }
            finally
            {
                progress1.Value = 0;
                cts.Dispose();
                cts = null;
                button1.Enabled = true;
                button2.Enabled = false;
            }
            Debug.Print($"{Thread.CurrentThread.ManagedThreadId}: Click End");
        };
        button2.Click += (s, e) =>
        {
            cts?.Cancel();
        };
    }
}//class

実行

dotnet run

実行ボタンを押します。

プログレスバーが一杯なると「完了」のメッセージが表示されます。

実行中に「キャンセル」ボタンを押すとTaskがキャンセルされます。

感想

キャンセルをCancellationTokenSourceを介してTaskに伝えています。キャンセルが通知されたら、OperationCanceledException例外をthrowしてTask内のループから一気に脱出しています。


自分の環境だけかもしれませんがvscodeでデバッグ実行すると、throwで例外がcatchできなかったとエラーが表示されます。

その後F5キーを押して続行すると例外がcatchしたメッセージボックスが表示されます。また、dotnet runで実行した場合は発生しないので、こういうものだと思うことにしています。

ソースコード2

using System.Diagnostics;

namespace AsyncSample2;

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        var panel1 = new FlowLayoutPanel {
            Dock = DockStyle.Fill,
            Padding = new Padding(10),
        };
        var button1 = new Button {
            Size = new Size(100, 50),
            Enabled = true,
            Text = "実行",
        };
        var button2 = new Button {
            Size = new Size(100, 50),
            Enabled = true,
            Text = "キャンセル",
        };
        var progress1 = new ProgressBar {
            Size = new Size(200, 40),
        };
        panel1.Controls.AddRange(new Control[]{
            button1, button2, progress1,
        });
        panel1.SetFlowBreak(button2, true);
        Controls.Add(panel1);
        IProgress<int> progress = new Progress<int>(x =>
        {
            progress1.Value = x;
        });
        CancellationTokenSource? cts = null;
        
        button1.Click += async (s, e) => {
            if (null != cts)
            {
                cts.Cancel();
                while(null != cts)
                {
                    await Task.Delay(10);
                }
            }
            cts = new CancellationTokenSource();
            var _task = Task.Run(()=>{
                
                for(int i = 0; i < 100; i++) {
                    cts.Token.ThrowIfCancellationRequested();
                    Task.Delay(10).Wait();
                    progress.Report(i+1);
                }
            }, cts.Token);;
            try {
                progress1.Value = 0;
                await _task;
            }
            catch (OperationCanceledException ex) {
                MessageBox.Show($"{ex.Message}", $"例外補足");
            }
            finally {
                this.Text = "";
                if (_task.IsCanceled)
                {
                    this.Text = this.Text + "キャンセル";
                }
                if (_task.IsCompletedSuccessfully)
                {
                    this.Text = this.Text + "成功完了";
                }
                cts?.Dispose();
                cts = null;
            }
        };
        button2.Click += (s, e) =>
        {
            cts?.Cancel();
        };
    }
}
実行中に「実行」ボタンを押すと再実行(キャンセル→実行)するサンプル

コメント