HeavyTask1, HeavyTask2 メソッドの引数で CancellationToken を受けて、キャンセルされたら CancellationToken.ThrowIfCancellationRequested() で OperationCanceledException をスローするようにし、ExecParallelTask メソッドでそれを catch するようにしてはいかがですか?
具体例は以下のコードを見てください。.NET Framework 4.8 の Windows Forms アプリです。
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsAsyncTest
{
public partial class Form5 : Form
{
private CancellationTokenSource cts = null;
public Form5()
{
InitializeComponent();
EableButtons();
}
// RunProcTaskAsync 起動
private async void button1_Click(object sender, EventArgs e)
{
DisableButtons();
this.textBox1.Text = "RunProcTaskAsync 起動";
var p = new Progress<string>(ShowProgress);
using (this.cts = new CancellationTokenSource())
{
// 起動してから 5 秒後にキャンセル
this.cts.CancelAfter(5000);
CancellationToken token = this.cts.Token;
try
{
await RunProcTaskAsync(p, token);
}
catch (OperationCanceledException)
{
// 必要なら何らかの処置
}
}
EableButtons();
}
// キャンセル
private void button2_Click(object sender, EventArgs e)
{
if (this.cts != null) cts.Cancel();
}
// RunProcTask 起動
private async void button3_Click(object sender, EventArgs e)
{
DisableButtons();
this.textBox1.Text = "RunProcTask 起動";
var p = new Progress<string>(ShowProgress);
using (this.cts = new CancellationTokenSource())
{
// 起動してから 5 秒後にキャンセル
this.cts.CancelAfter(5000);
CancellationToken token = this.cts.Token;
try
{
await Task.Run(() => RunProcTask(p, token));
}
catch (OperationCanceledException)
{
// 必要なら何らかの処置
}
}
EableButtons();
}
// 処理タスク
// ポーリングによるキャンセルのリッスン
// IProgress<T>/Progress<T> で進捗をレポート
private async Task RunProcTaskAsync(IProgress<string> progress, CancellationToken token)
{
int i = 0;
while (true)
{
token.ThrowIfCancellationRequested();
await Task.Delay(1000);
progress.Report(i.ToString());
i++;
}
}
private void RunProcTask(IProgress<string> progress, CancellationToken token)
{
int i = 0;
while (true)
{
token.ThrowIfCancellationRequested();
Thread.Sleep(1000);
progress.Report(i.ToString());
i++;
}
}
// 進捗を TextBox に表示するコールバック
// UIスレッドで呼び出される
private void ShowProgress(string log)
{
this.textBox1.Text = "ログ出力その " + log;
}
// ボタンの Enable / Disable を切り替えるヘルパメソッド
private void DisableButtons()
{
button1.Enabled = false;
button2.Enabled = true;
button3.Enabled = false;
}
private void EableButtons()
{
button1.Enabled = true;
button2.Enabled = false;
button3.Enabled = true;
}
}
}
実行結果は以下のようになります。[RunProcTask 起動]ボタンクリックで同期版のメソッド RunProcTask を Task.Run で起動し、そのボタンのハンドラに追加した this.cts.CancelAfter(5000); により 5 秒後にキャンセルされたところです。
【追記】
キャンセル処理の基本は Microsoft のドキュメント「マネージド スレッドのキャンセル」(URL 下記)に書いてありますので読んでみてください。
マネージド スレッドのキャンセル
https://docs.microsoft.com/ja-jp/dotnet/standard/threading/cancellation-in-managed-threads
それに書いてありますが、キャンセル処理を実装するための一般的なパターンは以下の通りだそうです。
- CancellationTokenSource クラスのインスタンスを作成する。
- CancellationTokenSource.Token プロパティで CancellationToken を取得し、キャンセルをリッスンするタスクに渡す。
- タスクにはキャンセル通知を適切に処置するコードを実装しておく。
- CancellationTokenSource.Cancel メソッドを呼び出し、リッスンしているタスクにキャンセルを通知する。
- キャンセル通知を受けたタスクは、あらかじめ実装されているコードに従ってキャンセル処置を行う。
タスクがキャンセルの通知を受けとるには、(1) ポーリング、(2) コールバックの登録、(3) 待機ハンドルの待機という方法があるそうです。上のコード例は (1) を使っています。