キャンセル ボタンは実行を停止すべきです。各プロキシ チェックがキャンセルを捕捉して正常に戻ると、外側のループは単に次のプロキシへ進みます。その結果、画面はキャンセルが何もしていないように見えます。

現代の .NET WinForms では、1 つのソースをフォーム フィールドとして保持し、キャンセルをイベント ハンドラーまで届けます。

private CancellationTokenSource? activeRun;

private async void startButton_Click(object sender, EventArgs e)
{
    if (activeRun is not null) return;
    using var run = new CancellationTokenSource();
    activeRun = run;
    startButton.Enabled = false;
    try
    {
        foreach (Uri proxy in configuredProxies)
        {
            run.Token.ThrowIfCancellationRequested();
            await CheckProxyAsync(proxy, run.Token);
        }
        statusLabel.Text = "Finished";
    }
    catch (OperationCanceledException) when (run.IsCancellationRequested)
    {
        statusLabel.Text = "Stopped";
    }
    finally
    {
        activeRun = null;
        startButton.Enabled = true;
    }
}

private void cancelButton_Click(object sender, EventArgs e)
    => activeRun?.Cancel();

configuredProxies はアプリがすでに解析したプロキシ URI のリストです。無料プロキシ リストの行を使って、許可された公開ターゲットのチェックに供給できます。プロトコルを維持し、このハンドラーがサポートする型だけを使用してください。UI は各操作を待機するため、メッセージ ループはキャンセル クリックを受け取れます。

トークンをネットワークまで運ぶ

個々のチェックはそのトークンを受け入れる必要があります。簡略化した HTTP チェックは次のようになります。

private async Task CheckProxyAsync(Uri proxy, CancellationToken token)
{
    using var handler = new HttpClientHandler
    {
        Proxy = new System.Net.WebProxy(proxy),
        UseProxy = true
    };
    using var client = new HttpClient(handler);
    using var response = await client.GetAsync(
        probeUri, HttpCompletionOption.ResponseHeadersRead, token);
    response.EnsureSuccessStatusCode();
    string body = await response.Content.ReadAsStringAsync(token);
    RecordResult(proxy, body);
}

probeUri と RecordResult は既存のチェッカーに属します。このエンドポイントには適切な応答サイズの上限とタイムアウトを設定してください。実運用のチェッカーはルートごとにクライアントを再利用できます。この断片はキャンセルがどこを通るかを示すもので、接続プールの設計ではありません。

失敗したプロキシを記録してスキップする場合は、個々のチェックの周囲で想定されるネットワーク障害を捕捉します。実行のキャンセルは外側のハンドラーへ逃がしてください。並列チェックでは、実行トークンを各ワーカーと共有し、別の実行を有効にする前に完了を待ちます。

関連記事