CancellationTokenSource 和 CancellationToken
CancellationTokenSource
CancellationTokenSource
是一个用于创建和控制CancellationToken
的类。它是触发取消操作的关键点,并且提供了触发取消操作的方法(如Cancel
)和检查是否已请求取消的属性(如IsCancellationRequested
)。
主要方法:
Cancel()
: 触发取消操作。Dispose()
: 释放CancellationTokenSource
占用的资源。
主要属性:
IsCancellationRequested
: 指示是否已请求取消。Token
: 获取与此CancellationTokenSource
关联的CancellationToken
。
CancellationToken
CancellationToken
是一个轻量级的对象,用于在线程之间传递取消信号。它本身不执行任何取消操作,而是作为取消请求的标记。当某个操作应该被取消时,与该CancellationToken
关联的CancellationTokenSource
会发出一个取消信号,然后任何监听这个CancellationToken
的代码都可以响应这个取消请求。
主要方法:
Register(Action callback)
: 注册一个回调,当取消操作被触发时调用该回调。ThrowIfCancellationRequested()
: 如果已请求取消,则抛出OperationCanceledException
异常。
主要属性:
IsCancellationRequested
: 指示是否已请求取消。
使用示例:
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var cts = new CancellationTokenSource();
var token = cts.Token;
var task = Task.Run(() => DoWork(token), token);
// 模拟一段时间后取消操作
Thread.Sleep(2000);
cts.Cancel();
try
{
await task; // 如果操作被取消,这里会抛出异常
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was canceled.");
}
}
static void DoWork(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
if (token.IsCancellationRequested)
{
Console.WriteLine("Cancellation requested.");
return;
}
Thread.Sleep(500); // 模拟工作
Console.WriteLine($"Working... {i}");
}
}
}
当使用 CancellationToken
与 Task.Run
时,有几个关键点需要注意:
-
传递给
Task.Run
的CancellationToken
:这个CancellationToken
用于控制Task.Run
创建的任务的生命周期。如果取消这个令牌(即调用CancellationTokenSource.Cancel()
),那么Task
将接收到一个取消请求,但这并不意味着任务会立即停止执行。任务中的代码需要显式检查这个取消请求(通常通过调用cancellationToken.ThrowIfCancellationRequested()
或检查cancellationToken.IsCancellationRequested
属性)并据此决定是否停止执行。 -
传递给
DoWork
方法的CancellationToken
:这个CancellationToken
是作为参数传递给DoWork
方法的。在DoWork
方法内部,你可以根据这个令牌的状态来决定是否继续执行。如果检测到取消请求(IsCancellationRequested
为true
),你可以提前退出方法或执行清理操作。但是,仅仅设置这个令牌的取消状态并不会自动停止DoWork
方法的执行;你需要编写相应的逻辑来处理取消请求。 -
任务的取消:即使你取消了
CancellationToken
,Task
对象本身也不会立即变为“已取消”状态。相反,它会变为“已取消请求”状态,这意味着已经请求取消该任务,但任务可能仍在执行中。只有当任务中的代码显式检查并响应取消请求时,任务才会停止执行并最终变为“已取消”状态。 -
异常处理:如果在任务中检测到取消请求并调用了
cancellationToken.ThrowIfCancellationRequested()
,则会抛出一个OperationCanceledException
异常。这个异常通常被视为正常的取消流程的一部分,而不是一个需要捕获和处理的错误。
因此,总结来说,当 CancellationToken
被取消时,Task.Run
创建的任务会接收到一个取消请求,但 DoWork
方法本身并不会自动停止执行。你需要在 DoWork
方法内部编写逻辑来响应这个取消请求并据此决定是否停止执行。同样,任务本身也不会立即停止执行;它将继续执行直到遇到检查取消请求的代码,并根据该代码的逻辑来决定是否停止。
CancellationToken取消了啥
在C#中,当DoWork
方法通过检查CancellationToken.IsCancellationRequested
属性并提前返回时,它只是从该方法中退出了。这并不直接结束或取消底层的Task
对象。但是,由于Task.Run
创建的Task
对象是与DoWork
方法的执行相关联的,因此当DoWork
方法返回时,该Task
对象会将其状态设置为“已完成”(RanToCompletion),而不是“已取消”(Canceled)。
然而,如果你想要在检测到取消请求时使Task
对象的状态变为“已取消”,你需要抛出一个OperationCanceledException
。这通常通过调用CancellationToken.ThrowIfCancellationRequested()
方法来实现,该方法在取消请求已发送时会抛出异常。
以下是修改后的DoWork
方法示例,它在检测到取消请求时抛出OperationCanceledException
:
static void DoWork(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested(); // 如果取消请求已发送,则抛出异常
Console.WriteLine($"DoWork: Working {i + 1}...");
Thread.Sleep(500); // 模拟耗时操作
}
Console.WriteLine("DoWork: Work completed."); // 这行代码实际上不会被执行,因为循环会在某个点抛出异常
}
在这个例子中,如果CancellationToken
的取消请求被发送(即调用了CancellationTokenSource.Cancel()
方法),ThrowIfCancellationRequested()
方法将抛出一个OperationCanceledException
异常。这个异常会冒泡到调用Task.Run
的代码,并最终导致Task
对象的状态变为“已取消”。
但是,请注意,即使你抛出了OperationCanceledException
,Task
对象中的任何本地资源(如文件句柄、数据库连接等)仍然需要由你的代码显式释放。异常只是改变了Task
的状态并通知调用者任务已取消,但它不会自动执行任何清理工作。
ManualResetEvent
ManualResetEvent
是一个同步基元,它允许线程通过信号进行通信。当事件处于未发出状态时,线程调用WaitOne
、WaitAny
或WaitAll
等方法时会阻塞,直到其他线程通过调用Set
方法将事件标记为已发出状态。事件保持已发出状态,直到调用Reset
方法将其重置为未发出状态。
主要方法:
Set()
: 将ManualResetEvent
设置为已发出状态,允许一个或多个等待的线程继续执行。Reset()
: 将ManualResetEvent
重置为未发出状态,阻塞等待的线程。WaitOne()
: 阻塞当前线程,直到ManualResetEvent
被设置为已发出状态。
使用示例:
创建了几个Task
,它们都等待同一个ManualResetEvent
被设置。一旦事件被设置,所有等待的Task
都会继续执行。
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static ManualResetEvent resetEvent = new ManualResetEvent(false);
static async Task Main(string[] args)
{
// 创建并启动多个Task
Task[] tasks = new Task[3];
for (int i = 0; i < tasks.Length; i++)
{
int taskId = i; // 捕获循环变量
tasks[i] = Task.Run(() => Worker(taskId));
}
// 模拟一些工作...
Console.WriteLine("Main thread is doing some work...");
await Task.Delay(2000); // 异步等待
// 设置事件,允许所有等待的Task继续执行
Console.WriteLine("Setting the event to allow all tasks to continue...");
resetEvent.Set();
// 等待所有Task完成
await Task.WhenAll(tasks);
Console.WriteLine("All tasks have completed.");
}
static void Worker(int taskId)
{
Console.WriteLine($"Task {taskId} is waiting...");
resetEvent.WaitOne(); // 等待事件被设置
Console.WriteLine($"Task {taskId} continues...");
// 模拟一些工作...
Thread.Sleep(1000); // 注意:在async方法中通常使用await Task.Delay()
}
}
上面的Worker
方法实际上是同步的,并且它使用了Thread.Sleep
来模拟工作,这通常不是async
/await
模式中的最佳做法。在async
方法中,你应该使用await Task.Delay()
来异步等待。但是,由于Worker
方法被设计为与ManualResetEvent
一起工作,并且是在Task.Run
中调用的,所以这里使用Thread.Sleep
是可以接受的。
如果你想要一个完全基于async
/await
的示例,并且不使用ManualResetEvent
(因为async
/await
提供了更自然的异步等待模式),你可以这样做:
using System;
using System.Threading.Tasks;
class Program
{
static Task completionSignal = new TaskCompletionSource<bool>().Task;
static async Task Main(string[] args)
{
// 创建并启动多个Task
Task[] tasks = new Task[3];
for (int i = 0; i < tasks.Length; i++)
{
tasks[i] = Task.Run(async () => await Worker(i));
}
// 模拟一些工作...
Console.WriteLine("Main thread is doing some work...");
await Task.Delay(2000);
// 设置事件,允许所有等待的Task继续执行
((TaskCompletionSource<bool>)completionSignal.Source).SetResult(true);
// 等待所有Task完成
await Task.WhenAll(tasks);
Console.WriteLine("All tasks have completed.");
}
static async Task Worker(int taskId)
{
Console.WriteLine($"Task {taskId} is waiting...");
await completionSignal; // 等待事件被设置
Console.WriteLine($"Task {taskId} continues...");
// 模拟一些异步工作...
await Task.Delay(1000);
}
}
在这个async
/await
示例中,我们使用TaskCompletionSource<T>
来创建一个可以等待的任务,并在适当的时候通过调用SetResult
来标记它已完成。这是async
/await
世界中同步多个异步操作的一种更自然的方式。
对于无限循环的线程,并且需要外部控制暂停/恢复的场景,使用ManualResetEvent
你可以根据需要多次设置和重置它。
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static ManualResetEvent pauseEvent = new ManualResetEvent(true); // 初始化为已设置状态,即线程不暂停
static void Main(string[] args)
{
// 启动工作线程
Task.Run(WorkerThread);
// 控制台输入来控制暂停和恢复
Console.WriteLine("Press 'p' to pause, 'r' to resume, or 'q' to quit.");
while (true)
{
var key = Console.ReadKey(true).KeyChar;
switch (key)
{
case 'p':
pauseEvent.Reset(); // 暂停线程
Console.WriteLine("Worker thread paused.");
break;
case 'r':
pauseEvent.Set(); // 恢复线程
Console.WriteLine("Worker thread resumed.");
break;
case 'q':
// 这里需要一种方式来通知工作线程优雅地退出,但在这个简单示例中我们直接退出程序
Environment.Exit(0);
break;
}
}
}
static void WorkerThread()
{
while (true)
{
// 等待暂停事件
pauseEvent.WaitOne();
// 执行工作...
Console.WriteLine("Worker thread is working...");
// 模拟工作耗时
Thread.Sleep(500);
// 注意:在真实场景中,你可能不需要在每次循环迭代中都调用 WaitOne,
// 除非你真的希望每次迭代都检查暂停状态。
// 在这个示例中,我们只是为了演示而这样做。
}
}
}
上面的代码示例有一个问题:它会在每次循环迭代中都检查暂停状态,这可能会导致不必要的性能开销。在实际应用中,你可能希望只在需要时才检查暂停状态,或者将工作分解为更大的块,并在每个块之后检查暂停状态。
AutoResetEvent
AutoResetEvent
和ManualResetEvent
都用于线程间的同步和通信,但它们之间存在一些关键区别:
- 重置行为:
AutoResetEvent
在释放一个等待线程后会自动重置为未设定状态,而ManualResetEvent
需要显式调用Reset()
方法才能重置为未设定状态。 - 信号通知:
AutoResetEvent
每次调用Set()
方法时,只允许一个等待线程被唤醒;而ManualResetEvent
在调用Set()
方法后,会唤醒所有等待的线程。
AutoResetEvent
和ManualResetEvent
(实际上是同一事物)的主要特点是其自动重置的行为,这使得它在控制线程执行顺序和同步线程操作方面非常有用,其他方法调用都和ManualResetEvent
一致。