我正在尝试创建一个异步运行的强制超时机制。 理想情况下,我需要运行一个任务,而不等待它,并且我希望在一段时间后它会抛出一个异常来模拟超时。
我的问题是我无法更改主线程中调用方法的方式,只能在其中添加方法。
我还知道,在未等待的异步任务中引发的异常永远不会在主线程中捕获。但这正是我所需要的,但找不到解决方案。
为了举例说明我的问题,请使用以下代码:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
//Main method which I don't have control over, to change how other methods are called, I can only add methods here.
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
try
{
//Forced timeout
Console.Write("Init timeout \n");
var forcedTimeout = myForcedTimeoutMethod(1000);
//Do something that might be long
//Just pretending the main thread is still doing some work that should take longer than 1 second
AsyncUtil.RunSync<bool>(() => ForcedTimeoutContext.Wait(2000));
Console.Write("Method finished \n");
forcedTimeout.Dispose();
}
catch (Exception e)
{
Console.Write("Exception caught: " + e.Message);
}
}
public static ForcedTimeoutContext myForcedTimeoutMethod(int TimeInMs)
{
ForcedTimeoutContext forcedTimeout = new ForcedTimeoutContext();
forcedTimeout.TokenSource = new CancellationTokenSource();
forcedTimeout.Task = Task.Run(async delegate
{
await Task.Delay(TimeSpan.FromMilliseconds(TimeInMs), forcedTimeout.TokenSource.Token);
forcedTimeout.TokenSource.Token.ThrowIfCancellationRequested();
throw new Exception("Forced timeout reached");
});
return forcedTimeout;
}
}
//Context class to be able to Dispose and cancel the timeout if needed
class ForcedTimeoutContext : IDisposable
{
public CancellationTokenSource TokenSource;
public Task Task;
public void Dispose()
{
if (TokenSource != null)
TokenSource.Dispose();
if (Task != null)
Task.Dispose();
}
public static async Task<bool> Wait(int ssms)
{
await Task.Delay(ssms);
return true;
}
}
//Utility helper to run async methods synchronously
public class AsyncUtil
{
private static readonly TaskFactory taskFactory = new TaskFactory(
CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);
public static TResult RunSync<TResult>(Func<Task<TResult>> task) =>
taskFactory
.StartNew(task)
.Unwrap()
.GetAwaiter()
.GetResult();
}
}
正如预期的那样,“myForcedTimeoutMethod”函数中的任务不会等待,“强制超时达到”异常永远不会被主线程捕获。
我也尝试使用 Task.Run().ContinueWith(...) ,但延续也在不同的范围内执行。
我能够实现的最接近的解决方案是使用 Thread.Interrupt() ,如下所示:
public static ForcedTimeoutContext myForcedTimeoutMethod(int TimeInMs)
{
ForcedTimeoutContext forcedTimeout = new ForcedTimeoutContext();
forcedTimeout.TokenSource = new CancellationTokenSource();
var currThread = Thread.CurrentThread;
forcedTimeout.Task = Task.Run(async delegate
{
await Task.Delay(TimeSpan.FromMilliseconds(TimeInMs), forcedTimeout.TokenSource.Token);
currThread.Interrupt();
});
return forcedTimeout;
}
}
但是,在这种情况下,我对异常没有任何控制权,我希望能够抛出特定的异常。
有没有办法在异步任务的主线程中抛出异常而不等待它?
你不能随便在不同的线程中引发异常。这个现实(至少部分)是为什么
CancellationToken
存在于async
代码中,这是一种跨流中断的协作形式 - 但它要求您想要能够中断的代码采取额外的步骤来允许自身被中断(通过预检查或委托回调)。
这里没有你想要编织的魔法。
假设:
cancellationToken
如果您使用的是.NET 6之前的版本
,您可以在
Task<TResult>
上使用类似于此的扩展方法(几乎可以肯定我偷了这个,但不记得来源)
public async static Task<TResult> WithTimeout<TResult>(
this Task<TResult> task, TimeSpan timeout) {
Task firstToFinish = await Task.WhenAny(task, Task.Delay(timeout))
.ConfigureAwait(false);
if (firstToFinish != task) throw new TimeoutException();
return await task.ConfigureAwait(false); // Unwrap result/re-throw
}
对于 .NET 6 及更高版本,正如 Theodor Zoulias 在评论中建议的那样,您可以使用 built-in
Task.WaitAsync
方法。它也提供了接受取消令牌的进一步重载,并且可能更高效、更快。
和一个使用示例:
try {
var result = await Task.Run(() => {
Thread.Sleep(2000);
return 3;
})
// .WithTimeout(TimeSpan.FromMilliseconds(1500)); // test with < and > 2000
.WaitAsync(TimeSpan.FromMilliseconds(1500)); //.NET 6+
Console.WriteLine(result);
} catch (TimeoutException ex) {
Console.WriteLine(ex.Message);
}