在C#中,可以使用CancellationToken
和Task
的超时机制来实现调用方法时的超时终止。
一
用Task.Delay(int)模拟耗时操作
static async Task Main(string[] args)
{
using (var cts = new CancellationTokenSource(1 * 1000))
{
await doSomething(cts.Token);
}
Console.WriteLine("Press any key to end...");
Console.ReadLine();
Console.WriteLine();
}
static async Task doSomething(CancellationToken cancellationToken)
{
try
{
int x = new Random().Next(4, 7);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} 大约需要{x}秒才能执行结束");
//模拟耗时操作
await Task.Delay(x * 1000, cancellationToken);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} 执行doSomething结束");
}
catch (OperationCanceledException ex)
{//如果被取消,则抛出异常
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} 超时了:" + ex.Message);
}
}
二
如果我们要设置超时的方法本身不支持异步或超时参数时,可以通过使用Task
和CancellationToken
来实现,但这需要一些间接的方式。
假设,方法doOperate(定义如下)执行时间较长,我们要在调用它时,设置超时操作。
static bool doOperate(int x, string y, out string error)
{
// 模拟长时间操作
System.Threading.Thread.Sleep(5000); // 假设操作需要5秒
error = null; // 假设没有错误
Console.WriteLine($"Hello {y}");
return true; // 假设操作成功
}
从doOperate的定义中,我们可以看到:doOperate方法并没有设计为异步且不接受超时或取消令牌。
这种情况下,如果我们想设置在调用doOperate方法时超时,一种常用的方法是将doOperate
的调用放在一个单独的任务中,并使用Task.Delay
和Task.WhenAny
来等待doOperate
或超时时间结束。
这里我们可以使用Task.Run
来封装doOperate
的调用。
但请注意,这可能会引入线程池的使用,如果doOperate
是CPU密集型的操作,可能会影响系统性能。如果doOperate
主要是IO操作(比如文件访问或数据库查询),这种方法的影响会比较小。
static void Main(string[] args)
{
MethodX();
}
static void MethodX()
{
string error = null;
bool result = CallDoOperateWithTimeout(3*1000, out error);
if (!result)
{
Console.WriteLine($"Operation failed: {error ?? "Timeout occurred."}");
}
else
{
Console.WriteLine("Operation completed successfully.");
}
}
static bool CallDoOperateWithTimeout(int timeoutMilliseconds, out string error)
{
var cts = new CancellationTokenSource(timeoutMilliseconds);
string tempError = null;
bool iResult = false;
Task task = Task.Run(() =>
{
try
{
doOperate(42, "someInput", out tempError);
iResult = true;
}
catch (Exception ex)
{
tempError = ex.Message;
iResult = false;
}
}, cts.Token);
try
{
task.Wait(cts.Token); // 等待任务完成或抛出异常
error = tempError;
return iResult;
}
catch (OperationCanceledException)
{
error = "Operation timed out.";
return false;
}
catch (Exception ex)
{
error = ex.Message;
return false;
}
}
执行后,我们会发现,确实提示”Operation failed: Operation timed out.“,但是也打印了”Hello somInput“——即doOperate
方法并没有被终止掉,这是因为:
doOperate
方法本身并不是真正的异步方法(即,它并没有使用 async
关键字,也没有 await
任何异步操作)。相反,它使用了 Thread.Sleep
来模拟长时间的操作,这是一个阻塞调用,会阻塞执行它的线程直到指定的时间过去。
当我们从 Main
方法或任何其他同步上下文中启动这个 Task
时,虽然 Task
本身是在一个单独的线程上执行的,但 doOperate
方法内的 Thread.Sleep
会阻塞那个单独的线程。与此同时,Main
方法中的代码会继续执行到 task.Wait(cts.Token)
,它等待 Task
完成或超时。
如果 Task
(即 doOperate
方法的执行)在超时之前还没有完成(在这个例子中是5秒),CancellationTokenSource
的 Token
将会被触发来请求取消操作。但是,请注意,doOperate
方法内部并没有检查 CancellationToken
的状态,因此它不会提前退出。因此,Thread.Sleep
将会继续执行直到其完成,随后 doOperate
方法将输出 "Hello {y}"
并返回 true
。
然而,在 Main
方法中,由于已经超过了超时时间,task.Wait(cts.Token)
会抛出一个 OperationCanceledException
(或者,如果 Task
实际上在超时之后完成了,则不会抛出异常,但在这个例子中它不会)。这个异常会被捕获,并且错误消息会被设置为 "Operation timed out."
。
要解决这个问题并让 doOperate
方法能够响应取消请求,您需要在 doOperate
方法内部定期检查 CancellationToken
的状态。
但是,由于 doOperate
使用了 Thread.Sleep
,而 Thread.Sleep
是不支持取消的,您需要使用其他方法来模拟异步操作,比如使用 Task.Delay
(它可以接受一个 CancellationToken
)或者实现您自己的异步逻辑(比如轮询某种条件或等待某个事件)。
然而,在这个特定的例子中,由于 doOperate
方法是同步的并且使用了 Thread.Sleep
,所以我们无法直接让它响应取消请求。
如果我们想要让 doOperate
能够被取消,我们需要重写doOperate
以使用异步模式,或者找到一种方法来避免在需要响应取消的场景中使用 Thread.Sleep
。
如果我们只是想在超时后停止等待 doOperate
的结果,并且不关心 doOperate
方法是否实际完成,那么我们的代码已经按预期工作了,只是 doOperate
会在后台继续执行直到完成。如果我们想要确保 doOperate
在超时后被取消(即停止执行),那么我需要重新设计 doOperate
方法以支持取消。
三
重写doOperate
方法,将它转变为一个异步方法,并使用 CancellationToken
来检查是否应该提前退出。
然而,由于原始的 doOperate
方法使用了 Thread.Sleep
来模拟长时间操作,这是不可取消的,我们需要找到一个替代方案。
一个常见的替代方案是使用 Task.Delay
,它是一个可取消的异步延时操作。下面是重写后的 doOperate
方法和相应的调用逻辑:
/// <summary>
/// 封装操作结果的类
/// </summary>
private class OperationResult
{
/// <summary>
/// 执行成功
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 执行发生异常时的错误消息
/// </summary>
public string Error { get; set; }
}
/// <summary>
/// 异步版本的doOperate方法
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
static async Task<OperationResult> doOperateAsync(int x, string y, CancellationToken cancellationToken)
{
try
{
// 使用Task.Delay模拟异步操作,该操作支持取消
await Task.Delay(5000, cancellationToken);
// 假设这是耗时的异步操作
Console.WriteLine($"Hello {y}");
return new OperationResult { Success = true, Error = null };
}
catch (OperationCanceledException)
{
// 如果操作被取消
return new OperationResult { Success = false, Error = "Operation was cancelled." };
}
catch (Exception ex)
{
// 捕获并处理其他可能的异常
return new OperationResult { Success = false, Error = ex.Message };
}
}
static async Task Main(string[] args)
{
await MethodXAsync();
Console.WriteLine("press any key to end...");
Console.ReadKey();
}
static async Task MethodXAsync()
{
//设置超时时间是3秒
OperationResult result = await CallDoOperateWithTimeoutAsync(3 * 1000);
if (!result.Success)
{
Console.WriteLine($"Operation failed: {result.Error ?? "Timeout occurred."}");
}
else
{
Console.WriteLine("Operation completed successfully.");
}
}
static async Task<OperationResult> CallDoOperateWithTimeoutAsync(int timeoutMilliseconds)
{
var cts = new CancellationTokenSource(timeoutMilliseconds);
try
{
// 注意:这里我们不需要将cts.Token传递给Task.Run,
// 因为我们是在等待DoOperateAsync的完成,而不是Task.Run的完成。
// Task.Run主要用于在后台线程上执行代码,但在这里我们直接调用异步方法。
var result = await doOperateAsync(42, "someInput", cts.Token);
return result;
}
catch (TaskCanceledException)
{
return new OperationResult { Success = false, Error = "Operation timed out." };
}
catch (Exception ex)
{
return new OperationResult { Success = false, Error = ex.Message };
}
}
请注意以下几点:
-
我将
Main
方法改为异步的,并使用了await
关键字来等待MethodXAsync
的完成。这是处理异步程序的常见做法。 -
CallDoOperateWithTimeoutAsync
方法直接调用doOperateAsync
并等待其完成,同时处理可能的取消异常和其他异常。doOperateAsync
方法现在是一个返回OperationResult
实例的异步方法,该实例包含了操作的成功状态和可能的错误消息。 -
DoOperateAsync
方法现在是一个异步方法,可以在不阻塞当前线程的情况下执行长时间的操作。
如果doOperateAsync
中的代码需要在另一个线程上执行(例如,因为它执行了阻塞的 I/O 操作),那么我们可以考虑使用Task.Run
来封装这部分代码。但是,在这个例子中,Task.Delay
已经是一个异步操作,所以我们不需要额外的线程。 -
我创建了一个
OperationResult
类来封装doOperate
方法的成功状态和可能的错误消息。这样,我们就可以在异步操作完成后返回一个包含这些信息的单一对象。
现在,当我们运行这个程序时,如果 doOperateAsync
方法在超时之前完成,它将输出 "Hello someInput"
并报告成功。如果超时发生,它将报告超时错误,并且 doOperateAsync
方法中的 Console.WriteLine
将不会被执行(因为 Task.Delay
会被取消)。