Taskクラス

System.Threading.Tasks.Task task = System.Threading.Tasks.Task.Run(() =>
{
    // 別スレッドでの処理
});

Taskクラスで作成したスレッドは、既定でバックグラウンド スレッドで実行されます。c# 4.0 - Are Tasks created as background threads? - Stack Overflow

インスタンス化

public Task(
    Action action
)
Task(Action) - Task コンストラクター (System.Threading.Tasks) | Microsoft Learn コンストラクター - Task クラス (System.Threading.Tasks) | Microsoft Learn

コンストラクタを呼ぶ以外に、TaskFactory.StartNew()やTask.Run()で生成する方法もあります。

Action action = () =>
{
    Console.Write("Task ID:{0}", Task.CurrentId);
};


// インスタンス化し、開始する
Task task1 = Task.Factory.StartNew(action);

// インスタンス化し、開始する (StartNew()の代替)
Task task2 = Task.Run(action);

// コンストラクタでインスタンス化する。そしてStart()で開始する
Task task3 = new Task(action);
task3.Start();

// コンストラクタでインスタンス化する。そしてRunSynchronously()でメインスレッドで同期実行する
Task task4 = new Task(action);
task4.RunSynchronously();
Task instantiation - Task Class (System.Threading.Tasks) | Microsoft Learn

Task.Run()にメソッド名で渡すとき、newを省略した書式ではFunc<Task>とのあいまいさを排除するために型名を指定します。

Task.Run(new Action(Method)); // OK
Task.Run(   (Action)Method); // OK
Task.Run(           Method); // error CS0121: 次のメソッドまたはプロパティ間で呼び出しが不適切です: 'Task.Run(Action)' と 'Task.Run(Func<Task>)'

値を渡す

デリゲートに値を渡す場合には、次のようにします。

Action<object> action = (obj) =>
{
};

Task task1 = Task.Factory.StartNew(action, 1);
// Task task2 = Task.Run(action, 2); // 不可

Task task3 = new Task(action, 3);
task3.Start();

// 渡した値は、AsyncStateプロパティから取得できる
object obj1 = task1.AsyncState; // 1
object obj3 = task3.AsyncState; // 3

扱う値が複数あるならば、Tupleクラス (.NET Fremework 4.7以降ならばValueTuple構造体) を用いると簡単です。Tuple クラス (System) | Microsoft Learn

Action<object> action = (obj) =>
{
    Tuple<int, string> tuple = (Tuple<int, string>)obj;
    int num = tuple.Item1;
    string str = tuple.Item2;
};

Task task = Task.Factory.StartNew(action, Tuple.Create(1, "a"));

値を戻す

デリゲートから値を受け取る場合には、Task<TResult>クラスを用います。Task<TResult> クラス (System.Threading.Tasks) | Microsoft Learn

Func<int> action = () =>
{
    return 1;
};

// 
Task<int> task1 = Task.Factory.StartNew(action);
int result1 = task1.Result; // 結果が返されるまで、呼び出し元のスレッドはブロックされる

// 
Task<int> task2 = Task.Run(action);
int result2 = task2.Result;

// 
Task<int> task3 = new Task<int>(action);
task3.Start();
int result3 = task3.Result;

このとき呼び出し元のスレッドをブロックしないようにするには、awaitで結果を得ます。

Task<int> task1 = Task.Factory.StartNew(action);
int result1 = await task1;

値を渡して戻す

デリゲートに値を渡して受け取る場合には、次のようにします。

Func<object, int> action = (obj) =>
{
    return 1;
};

// 
Task<int> task1 = Task.Factory.StartNew(action, 1);
int result1 = task1.Result;

...

プロパティ

プロパティ 内容
AggregateException Exception Taskが早く終了する原因を表すAggregateException
TaskFactory Factory ファクトリ メソッド
int Id Taskに割り当てられた識別子。作成順の番号となるとは限らず、一意であることも保証されない
Task CompletedTask StatusがRanToCompletionなタスク
TaskStatus Status タスクの状態
bool IsCompleted trueならば、Taskが完了している。完了とはStatusがRanToCompletion、Faulted、Canceledの3つの状態のいずれか
bool IsCanceled trueならば、Taskがキャンセルされたことによって完了している。これは次のいずれかの状況を表す。
  • Taskが開始される前にCancellationTokenがマークされている。
  • Taskに関連付けられたCancellationTokenからOperationCanceledExceptionが投げられたことで、キャンセルが要求されている。
Remarks - Task.IsCanceled Property (System.Threading.Tasks) | Microsoft Learn
object? AsyncState Taskが作成されたときに供給された状態オブジェクト。このオブジェクトは、TaskコンストラクタやTaskFactory.StartNew()メソッドで供給できる
プロパティ - Task クラス (System.Threading.Tasks) | MSDN

Status

TaskStatus列挙型
列挙子 数値 意味
Created 0 タスクは初期化されているが、まだスケジュールされていない
WaitingForActivation 1 タスクはアクティブ化されるのを待機中で、.NET Frameworkインフラストラクチャによって内部的にスケジュールされている

これはTask.ContinueWith()で作成されたタスク About continuations - Chaining tasks using continuation tasks - .NET | Microsoft Learn

WaitingToRun 2 タスクの実行はスケジュールされているが、まだ開始されていない
Running 3 タスクは実行中で、まだ完了していない
WaitingForChildrenToComplete 4 タスクは終了し、アタッチされている子タスクの完了を無条件に待機している
RanToCompletion 5 タスクは正常に完了した
Canceled 6 タスクのCancellationTokenがシグナル状態であるときに、タスクがこのトークンを使用してOperationCanceledExceptionをスローすることで取り消しを認めたか、タスクの実行開始前にタスクのCancellationTokenが既にシグナル状態だった
Faulted 7 タスクはハンドルされていない例外のために終了した
TaskStatus 列挙型 (System.Threading.Tasks) | Microsoft Learn

メソッド

戻り値 メソッド 機能
ConfiguredTaskAwaitable ConfigureAwait(Boolean) Taskを待機する方法を構成する
Task ContinueWith(Action<Task>, TaskContinuationOptions) Taskが完了したときに実行される、継続タスクを作成できる
Task Delay(Int32) 遅延した後に完了する、タスクを作成する
Task Run(Action) 指定の処理をスレッドプールに並ばせ、その処理を表すTaskを返す。TaskScheduler.Defaultを用いる
void Start() タスクを開始し、TaskScheduler.Currentにその実行を予定する
void RunSynchronously() TaskScheduler.Currentで同期的にタスクを実行する
void Wait() タスクの実行が完了するまで、待機する
bool Wait(Int32) タスクの実行が完了するまで、指定時間待機する
void WaitAll(Task[]) すべてのタスクの実行が完了するまで、待機する
int WaitAny(Task[]) いずれかのタスクの実行が完了するまで、待機する
Task WhenAll(Task[]) すべてのタスクの実行が完了したときに完了する、タスクを作成する
Task<Task> WhenAny(Task[]) いずれかのタスクの実行が完了したときに完了する、タスクを作成する
メソッド - Task クラス (System.Threading.Tasks) | MSDN

Run()

.NET Framework 4.5以降は、引数や細かい制御が不要ならばTaskFactory.StartNew()の代わりにこのメソッドを用います。逆にそれらが必要ならば、StartNew()を用います。Remarks - TaskFactory.StartNew Method (System.Threading.Tasks) | Microsoft Learn

public static Task Run(
    Action action
)
Run(Action) - Task.Run メソッド (System.Threading.Tasks) | Microsoft Learn

ただしStartNew()とRun()では、既定の設定が異なりますTask.Run vs Task.Factory.StartNew - .NET Parallel Programming - Site Home - MSDN Blogs

public Task StartNew(Action action)
{
    StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
    Task currTask = Task.InternalCurrent;

    return Task.InternalStartNew(currTask, action, null, m_defaultCancellationToken, GetDefaultScheduler(currTask),
        m_defaultCreationOptions, InternalTaskOptions.None, ref stackMark);
}
StartNew - TaskFactory.cs
public static Task Run(Action action)
{
    StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;

    return Task.InternalStartNew(null, action, null, default(CancellationToken), TaskScheduler.Default,
        TaskCreationOptions.DenyChildAttach, InternalTaskOptions.None, ref stackMark);
}
Run - Task.cs

よってRun()と同等の処理を期待するならば、引数を明示します。StartNew is Dangerous

Task.Factory.StartNew(() =>
{
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

StartNew()では引数を指定しないとTaskScheduler.Currentが用いられるため、コンテキストによっては呼び出し元と同じスレッドで実行されます。たとえば同期コンテキストに関連付けられたTask内で実行したときは、同じとなります。

// (ここはUIスレッド)

int id1, id2, id3, id4, id5, id6, id7, id8, id9;

id1 = Thread.CurrentThread.ManagedThreadId; // 10
await Task.Run(() =>
{
    id2 = Thread.CurrentThread.ManagedThreadId; // 6
}).ContinueWith((_task) =>
{
    id3 = Thread.CurrentThread.ManagedThreadId; // 10

    // Task.Run() で開始
    Task.Run(() =>
    {
        id4 = Thread.CurrentThread.ManagedThreadId; // 6
    });

    // TaskFactory.StartNew() で開始
    Task.Factory.StartNew(() =>
    {
        id5 = Thread.CurrentThread.ManagedThreadId; // 10 (呼び出し元と同じスレッド)
    });

    Task.Factory.StartNew(() => // TaskScheduler.Default を指定
    {
        id6 = Thread.CurrentThread.ManagedThreadId; // 11
    }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);


    // Task.Start() で開始
    new Task(() =>
    {
        id7 = Thread.CurrentThread.ManagedThreadId; // 10 (呼び出し元と同じスレッド)
    }).Start();

    new Task(() => // TaskScheduler.Default を指定
    {
        id8 = Thread.CurrentThread.ManagedThreadId; // 6
    }).Start(TaskScheduler.Default);


    // Thread.Start() で開始
    new Thread(() =>
    {
        id9 = Thread.CurrentThread.ManagedThreadId; // 16
    }).Start();

}, TaskScheduler.FromCurrentSynchronizationContext()); // 同期コンテキストでの実行を指示

Run<TResult>(Func<TResult>)

指定の処理をスレッドプールに並ばせ、その処理を表すTask<TResult>を受け取れます。

public static Task<TResult> Run<TResult>(
    Func<TResult> function // 非同期に実行する処理
)
Run<TResult>(Func<TResult>) - Task.Run メソッド (System.Threading.Tasks) | Microsoft Learn

戻り値のTask<TResult>.Resultから、結果を得られます。

FromResult()

FromResult()は、結果だけを返すときに用います。How to: Create Pre-Computed Tasks | Microsoft Learn .net - What is the use for Task.FromResult<TResult> in C# - Stack Overflow

public static Task<TResult> FromResult<TResult>(
    TResult result // 完了したタスクに格納する結果
)
Task.FromResult(TResult) メソッド (TResult) (System.Threading.Tasks) | MSDN
int cache = 0;

public Task<int> Method1()
{
    if (cache != 0)
    {
        // return cache * 2;
        // error CS0029: 型 'int' を 'System.Threading.Tasks.Task<int>' に暗黙的に変換できません

        return Task.FromResult(cache * 2);
    }
    else
    {
        return Task.Run(() =>
        {
            cache = 10;
            return cache;
        });
    }
}

public async void Method2()
{
    int a1 = await Method1(); // 10が返される
    int a2 = await Method1(); // 20が返される
}

Start()

public void Start (System.Threading.Tasks.TaskScheduler scheduler);
Start(TaskScheduler) - Task.Start メソッド (System.Threading.Tasks) | Microsoft Learn

schedulerで、Taskに関連付けるTaskSchedulerを指定できます。これを省略するとTaskScheduler.Currentが指定されます。Start - Task.cs

Taskを実行できるのは1度だけです。くり返し実行すると「既に開始したタスクで Start を呼び出すことはできません。」や「完了したタスクで Start を呼び出すことはできません。」としてInvalidOperationExceptionが投げられます。Remarks - Task.Start Method (System.Threading.Tasks) | Microsoft Learn

Task task = new Task(() => { });
task.Start();
task.Start(); // 「既に開始したタスクで Start を呼び出すことはできません。」
Task task = new Task(() => { });
task.Start();
task.Wait();

task.Start(); // 「完了したタスクで Start を呼び出すことはできません。」
Task.Delay(1).Start(); // 「保証型のタスクで Start を呼び出すことはできません。」

Wait()

タスクが完了するまで、呼び出し元のスレッドをブロックします。

public void Wait()
Wait() - Task.Wait メソッド (System.Threading.Tasks) | Microsoft Learn

これにより呼び出し元のスレッドと同期させられます。なお完了前にタスクがキャンセルされたときには、AggregateException例外が投げられます。

Task task = Task.Run(() =>
{
});

task.Wait(); // 呼び出し元のスレッドをブロックし、タスクの完了まで待機する

// これ以降はタスクの完了後に処理される

このメソッドの呼び出し前にタスクが完了していると、待ちません。一方で開始されていないと、永久に待ちます。Task.Wait and “Inlining” - .NET Parallel Programming

Task task = new Task(() => { });
task.Wait();

スレッドがブロックされることを望まず非同期で処理するならば、awaitで待機させます。c# - await vs Task.Wait - Deadlock? - Stack Overflow

await Task.Run(() => // 呼び出し元のスレッドはブロックされない
{
});

// これ以降はタスクの完了後に処理される

非同期メソッドが値を返すならば、Task.Wait()ではなくTask<TResult>.Resultプロパティのgetアクセサにアクセスすることで同期させられます。Remarks - Task<TResult>.Result Property (System.Threading.Tasks) | Microsoft Learn

Task<int> task = Task.Run(() =>
{
    return 1;
});

// 呼び出し元のスレッドをブロックし、タスクの完了まで待機する
int result = task.Result;

Wait(CancellationToken)

public void Wait (System.Threading.CancellationToken cancellationToken);
Wait(CancellationToken) - Task.Wait Method (System.Threading.Tasks) | Microsoft Learn

引数にCancellationTokenをとるWait()では、タスクが完了するか、キャンセル トークン (cancellation token) がキャンセルされるまで待機します。キャンセル トークンがキャンセルされたときには、このメソッドはOperationCanceledExceptionを投げます。

Wait(Int32)

public bool Wait (int millisecondsTimeout);

指定時間内にタスクが完了しないと、falseが返されます。またタスクがキャンセルされるとInnerExceptionsにTaskCanceledExceptionを含む、AggregateExceptionが投げられます。

WaitAll(Task[])

すべてのタスクが完了するまで、呼び出し元のスレッドをブロックします。

public static void WaitAll (params System.Threading.Tasks.Task[] tasks);
WaitAll(Task[]) - Task.WaitAll メソッド (System.Threading.Tasks) | Microsoft Learn
Task task1 = Task.Run(() => { });
Task task2 = Task.Run(() => { });

Task.WaitAll(task1, task2); // すべてのタスクが完了するまで、ここで待機する

tasksにnullが含まれていると、「タスク配列に少なくとも 1 つの null 要素が含まれました。」としてArgumentExceptionが投げられます。そのような状況が生じる恐れがあるならば、実行が不要な要素はTask.CompletedTaskを代入しておきます。

WhenAll()

すべてのタスクが完了したときに完了するタスクを得られます。

public static System.Threading.Tasks.Task WhenAll (params System.Threading.Tasks.Task[] tasks);
WhenAll(Task[]) - Task.WhenAll メソッド (System.Threading.Tasks) | Microsoft Learn
Task task1 = Task.Run(() => { });
Task task2 = Task.Run(() => { });

await Task.WhenAll(task1, task2);

// すべてのタスクが完了した後に、ここが処理される

ContinueWith()

Taskが完了したときに実行される、継続タスク (continuation task) を作成できます。この継続タスクはコンテキストによっては新しいスレッドで実行されます。

int id1, id2, id3;

id1 = Thread.CurrentThread.ManagedThreadId; // 10
Task.Run(() =>
{
    id2 = Thread.CurrentThread.ManagedThreadId; // 6
}).ContinueWith((_task) =>
{
    id3 = Thread.CurrentThread.ManagedThreadId; // 12
});

継続タスクを作成前にTaskが完了していても、実行されます。

Task task = new Task(() =>
{
});

task.Start();
task.Wait(); // Taskは完了する

task.ContinueWith((_task) =>
{
    // 実行される
});
public System.Threading.Tasks.Task ContinueWith (
    Action<System.Threading.Tasks.Task> continuationAction
    );

実行するアクションしか指定しなかったときは、TaskSchedulerはCurrent、CancellationTokenはdefault(CancellationToken)、TaskContinuationOptionsにはNoneが指定されます。ContinueWith - Task.cs

実行条件の指定

public System.Threading.Tasks.Task ContinueWith (
    Action<System.Threading.Tasks.Task> continuationAction,            // 実行するアクション
    System.Threading.Tasks.TaskContinuationOptions continuationOptions // タスクの動作条件
    );
ContinueWith(Action<Task>, TaskContinuationOptions) - Task.ContinueWith Method (System.Threading.Tasks) | Microsoft Learn

continuationOptionsを省略するとTaskContinuationOptions.Noneが指定さます。また戻り値は、タスクが完了したときにcontinuationOptionsの条件が満たされるならば実行されるTaskです。

Task.Run(() =>
{
}).ContinueWith((_task) =>
{
    // 捕捉されない例外があったときに、ここが実行される
}, TaskContinuationOptions.OnlyOnFaulted);
TaskContinuationOptions 列挙型
列挙子 内容
None 0 既定の動作。タスクが完了したとき、Statusプロパティの値は無関係にアクションが実行される
OnlyOnFaulted 327680 捕捉されない例外があったときに、アクションが実行される
OnlyOnRanToCompletion 393216 タスクが完了したときにStatusプロパティがRanToCompletionのときに、アクションが実行される
     
TaskContinuationOptions 列挙型 (System.Threading.Tasks) | Microsoft Learn

スケジューラの指定

継続タスクを実行するスケジューラを指定できます。

public System.Threading.Tasks.Task ContinueWith (
    Action<System.Threading.Tasks.Task> continuationAction,
    System.Threading.Tasks.TaskScheduler scheduler // 継続タスクに関連付けられ、その実行に用いられるTaskScheduler
    );
ContinueWith(Action<Task>, TaskScheduler) - Task.ContinueWith Method (System.Threading.Tasks) | Microsoft Learn
// (ここはUIスレッド)

int id1, id2, id3;

id1 = Thread.CurrentThread.ManagedThreadId; // 10
Task.Run(() =>
{
    id2 = Thread.CurrentThread.ManagedThreadId; // 6
}).ContinueWith((_task) =>
{
    id3 = Thread.CurrentThread.ManagedThreadId; // 10 (呼び出し元と同一のスレッド)
}, TaskScheduler.FromCurrentSynchronizationContext());

ConfigureAwait()

Taskをawaitで待機するのに使用される、awaiterを設定できます。

public ConfiguredTaskAwaitable ConfigureAwait(
    bool continueOnCapturedContext
)
Task.ConfigureAwait メソッド (System.Threading.Tasks) | Microsoft Learn

continueOnCapturedContextをfalseとすると、Taskの待機後に呼び出し元のスレッドに戻されなくなります。

await Task.Run(() =>
{
    // ここは別スレッド
}).ConfigureAwait(false);

// 呼び出し元へは戻されず、Task内と同一のスレッド。よってTaskの処理をインラインで記述するならば、そこの末尾に記述しても同じ

ConfigureAwait()を呼ばないのはConfigureAwait(true)として呼び出すのと同じで、非同期コンテキスト (async context) によってはTaskの待機後に呼び出し元のスレッドに戻されます。

このConfigureAwait()を呼ばない場合の既定の動作はパフォーマンス上のコストや、UIスレッドでのデッドロックを発生させる恐れがあります。よってその必要性がないならば、このメソッドでfalseを指定します。 Task.ConfigureAwait Method (System.Threading.Tasks) | Microsoft Learn ConfigureAwait FAQ - .NET Blog

await Task.Run(() =>
{
    // ここは別スレッド
});

// 非同期コンテキストによっては、呼び出し元のスレッドに戻される

ConfigureAwait()を呼ばないことによるデッドロック

awaitで処理を戻すTaskの完了を、Wait()で待たせるとデッドロックします。

Func<Task> func = async () =>
{
    await Task.Delay(1);
    // awaitがあるため呼び出し元のスレッドへ戻そうとするが、
    // そのスレッドはこのスレッドの完了をWait()で待機しているため、戻せない
};

func().Wait();
Func<Task> func = async () =>
{
    await Task.Delay(1).ConfigureAwait(false);
    // ここはTaskと同一のスレッドで実行されるため、このままこのスレッドの処理は完了する
};

func().Wait();
c# - Any difference between "await Task.Run(); return;" and "return Task.Run()"? - Stack Overflow

呼び出し元のスレッドに確実に戻す

ConfigureAwait(true)ではコンテキストによっては呼び出し元のスレッドに戻されないことがあるため、より確実にはスケジューラを指定するContinueWith()を用います。

await Task.Run(() =>
{
}).ContinueWith((_task) =>
{
    // ここは呼び出し元のスレッド
}, TaskScheduler.FromCurrentSynchronizationContext()).ConfigureAwait(false);

// Task内と同一のスレッド

タスクが完了しているならば、ConfigureAwait(false)の後も呼び出し元のスレッドと同一

タスクが完了しているならば呼び出し元のスレッドに戻す必要がないため、ConfigureAwait(false)の後も呼び出し元のスレッドと同一となります。

int id1 = Thread.CurrentThread.ManagedThreadId; // 10
await Task.Delay(100).ConfigureAwait(false);

int id2 = Thread.CurrentThread.ManagedThreadId; // 12
int id1 = Thread.CurrentThread.ManagedThreadId; // 10
await Task.CompletedTask.ConfigureAwait(false);

int id2 = Thread.CurrentThread.ManagedThreadId; // 10 (スレッドが変更されていない)

単体テストでは、呼び出し元のスレッドに戻らない

単体テストから実行した場合、ConfigureAwait(true)以降も呼び出し元のスレッドに戻らないことがあります。c# - Async does not continue on same thread in unit test - Stack Overflow

int id1, id2, id3;

id1 = Thread.CurrentThread.ManagedThreadId; // 9
await Task.Run(() =>
{
    id2 = Thread.CurrentThread.ManagedThreadId; // 10
}).ConfigureAwait(true);

id3 = Thread.CurrentThread.ManagedThreadId; // 10 (ここでは非同期メソッドと同一のスレッドとなっている)

Delay()

指定時間後に完了するタスクを作成できます。

public static System.Threading.Tasks.Task Delay (int millisecondsDelay);
Delay(Int32) - Task.Delay メソッド (System.Threading.Tasks) | Microsoft Learn

現在のスレッドをブロックしても良いならば、Wait()で待ちます。これはThread.Sleep()と同等です。

Task.Delay(1000).Wait();

ブロックしないならば、awaitで制御を戻します。

await Task.Delay(1000);

遅延中にキャンセルできるようにするには、CancellationTokenを渡します。

CancellationTokenSource tokenSource = new CancellationTokenSource();

Task.Run(async () =>
{
    await Task.Delay(1000, tokenSource.Token); // キャンセルされると「タスクが取り消されました。」としてTaskCanceledExceptionが投げられる
    // キャンセルされると、ここには到達しない
});

// 時間のかかる処理

tokenSource.Cancel(); // キャンセルを要求する
Delay(Int32, CancellationToken) - Task.Delay メソッド (System.Threading.Tasks) | Microsoft Learn

Dispose()

public void Dispose ();
Task.Dispose Method (System.Threading.Tasks) | Microsoft Learn

Dispose()を呼び出せるのは、StatusプロパティがRanToCompletion、Faulted、またはCanceledの状態であるとき、つまりIsCompletedがtrueのときのみです。それ以外のときにDispose()を呼ぶと、「タスクを破棄できるのは、そのタスクが完了状態 (RanToCompletion、Faulted、または Canceled) の場合だけです。(A task may only be disposed if it is in a completion state (RanToCompletion, Faulted or Canceled).)」としてInvalidOperationExceptionが投げられます。

基本的にDispose()を呼び出す必要はありません。Do I need to dispose of Tasks? | Parallel Programming with .NET

タスクのキャンセル

キャンセルを要求する側はCancellationTokenSource.Cancel()を呼び出し、別スレッドの側はそれのCancellationToken.IsCancellationRequestedでキャンセルの要求を検出し処理を中断します。

CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;

Task.Run(() =>
{
    if (token.IsCancellationRequested)
    {
        // キャンセルを要求されたならば、処理を抜ける
        break;
    }
});

// キャンセルを要求する
tokenSource.Cancel();

一度キャンセルを要求したCancellationTokenSourceはその要求を取り消せないため、再度CancellationTokenSourceが必要なときはそのインスタンスを再生成します。

CancellationTokenSourceへの最後の参照を解放する前に、CancellationTokenSource.Dispose()を呼びます。さもなくばガベージ コレクタがFinalize()を呼ぶまで、そのリソースは解放されません。 Dispose() - CancellationTokenSource.Dispose Method (System.Threading) | Microsoft Learn c# - When to dispose CancellationTokenSource? - Stack Overflow

ただしDispose()の後にCancel()を呼ぶとObjectDisposedExceptionが投げられるため、それへの対処が必要です。

キャンセルの通知

キャンセルしたことを呼び出し側へ通知したいならば、OperationCanceledExceptionを投げることでタスクを終了させます。そうするとStatusプロパティはTaskStatus.Canceledとなり、TaskContinuationOptions.OnlyOnCanceledを指定したContinueWith()で継続タスクを実行できます。

OperationCanceledExceptionはタスクのスレッドから投げられないと捕捉されず、継続タスクも実行されません。

CancellationToken.ThrowIfCancellationRequested()を呼び出してキャンセルさせる方法もありますが、これは

if (token.IsCancellationRequested)
    throw new OperationCanceledException(token);

とすることと同義です。CancellationToken.ThrowIfCancellationRequested Method (System.Threading) | Microsoft Learn

OperationCanceledExceptionが投げられるTaskは、そのコンストラクタでCancellationTokenを渡します。これはキャンセルできたことをTask.IsCanceledで検証するときにも有用ですが、これらが不要ならば渡す必要はありません。c# - Cancellation token in Task constructor: why? - Stack Overflow

public Task (
    Action action,
    System.Threading.CancellationToken cancellationToken
    );
Task(Action, CancellationToken) - Task Constructor (System.Threading.Tasks) | Microsoft Learn

CancellationTokenをTaskのコンストラクタで渡さないとキャンセルと見なされずStatusはFaultedとなり、継続タスクはOnlyOnCanceledではなくOnlyOnFaultedが実行されます。

CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;

Task task = new Task(() =>
{
    token.ThrowIfCancellationRequested();
    // token.IsCancellationRequested が true ならば、
    // OperationCanceledExceptionが投げられる

}, token); // CancellationTokenを渡さねばならない

task.ContinueWith((_task) =>
{
    // キャンセルされたときに、ここが実行される
}, TaskContinuationOptions.OnlyOnCanceled);

時間指定によるキャンセル

CancellationTokenSourceを作成するときに時間を指定すると、一定時間後に自動でキャンセルが要求されるようにできます。

キャンセルが要求されるだけで、指定の時間でキャンセルされるわけではありません。

public CancellationTokenSource (int millisecondsDelay);
CancellationTokenSource(Int32) - CancellationTokenSource Constructor (System.Threading) | Microsoft Learn

コンストラクタの呼び出し時点から秒読みが開始され、指定の時間が経過するとToken.IsCancellationRequestedがtrueに設定されます。この時間は、CancellationTokenSource.CancelAfter()で後から変更できます。

int millisecondsDelay = 100;

CancellationTokenSource tokenSource = new CancellationTokenSource(millisecondsDelay);
CancellationToken token = tokenSource.Token;

Task task = new Task(() =>
{
    if (token.IsCancellationRequested)
    {
    }
});

先行する処理のキャンセル

連続する要求に対し、最後の1つだけを処理する方法を考えます。

CancellationTokenSource tokenSource = null;

void Method()
{
    CancellationTokenSource newTokenSource = new CancellationTokenSource();
    CancellationTokenSource oldTokenSource = Interlocked.Exchange(ref tokenSource, newTokenSource);

    if (oldTokenSource != null)
    {
        oldTokenSource.Cancel(); // キャンセルを要求する
    }

    try
    {
        // 待機する
        await Task.Delay(1000, newTokenSource.Token); // キャンセルが要求されたならばTaskCanceledExceptionが投げられる

        // 何らかの処理
    }
    catch (TaskCanceledException)
    {
    }    
}
Microsoft Learnから検索