监控同步方法的超时。

17
我正在寻找一种有效的方法,在同步方法执行时间过长时抛出超时异常。我已经看过一些示例,但都没有完全符合我的需求。
我需要做到以下几点:
1. 检查同步方法是否超出其SLA 2. 如果超时,则抛出超时异常
不需要在同步方法执行时间过长时终止它。(多个失败将会触发熔断器并防止级联故障)
迄今为止,我的解决方案如下。请注意,我向同步方法传递了一个CancellationToken,希望它能够在超时时响应取消请求。此外,我的解决方案返回一个任务,然后可以按照调用代码的要求进行等待等操作。
我的担忧是,这段代码每个被监视的方法都会创建两个任务。我认为TPL会很好地处理这个问题,但我想确认一下。
这样做有意义吗?有更好的方法吗?
private Task TimeoutSyncMethod( Action<CancellationToken> syncAction, TimeSpan timeout )
{
  var cts = new CancellationTokenSource();

  var outer = Task.Run( () =>
  {
     try
     {
        //Start the synchronous method - passing it a cancellation token
        var inner = Task.Run( () => syncAction( cts.Token ), cts.Token );

        if( !inner.Wait( timeout ) )
        {
            //Try give the sync method a chance to abort grecefully
            cts.Cancel();
            //There was a timeout regardless of what the sync method does - so throw
            throw new TimeoutException( "Timeout waiting for method after " + timeout );
        }
     }
     finally
     {
        cts.Dispose();
     }
  }, cts.Token );

  return outer;
}

编辑:

使用@Timothy的答案,我现在正在使用这个。虽然代码量没有显著减少,但更加清晰易懂。谢谢!

  private Task TimeoutSyncMethod( Action<CancellationToken> syncAction, TimeSpan timeout )
  {
    var cts = new CancellationTokenSource();

    var inner = Task.Run( () => syncAction( cts.Token ), cts.Token );
    var delay = Task.Delay( timeout, cts.Token );

    var timeoutTask = Task.WhenAny( inner, delay ).ContinueWith( t => 
      {
        try
        {
          if( !inner.IsCompleted )
          {
            cts.Cancel();
            throw new TimeoutException( "Timeout waiting for method after " + timeout );
          }
        }
        finally
        {
          cts.Dispose();
        }
      }, cts.Token );

    return timeoutTask;
  }

你正在使用.NET 4.5和async/await吗? - Reed Copsey
https://dev59.com/j3VC5IYBdhLWcg3wbgdS - Robert Harvey
罗伯特:谢谢,我担心的是Thread.Abort()。我不想这样做。看起来太过激烈了。在我的情况下,我不需要中止。 - Andre
@Andre: 你应该优先选择Task.Run而不是TaskFactory.StartNew。请参考Stephen Toub的博客,如果这还不够有说服力,可以查看我的博客 - Stephen Cleary
@Stephen:好观点。谢谢。我会更新这个例子。 - Andre
4个回答

27
如果您有一个名为 taskTask,您可以这样做:
var delay = Task.Delay(TimeSpan.FromSeconds(3));
var timeoutTask = Task.WhenAny(task, delay);
如果timeoutTask.Result最终是task,那么它没有超时。否则,它是delay并且已经超时。 我不知道这是否与您实现的内容完全相同,但这是内置的方法来执行此操作。

1
谢谢,看起来整洁多了。我会看看如何使用它来获得类似的行为,如果一切顺利,我会接受这个答案。 - Andre

2

我已经重新编写了这个解决方案,以适应不可用一些方法的.NET 4.0版本,例如Delay。此版本正在监视返回object的方法。关于如何在.NET 4.0中实现Delay,请参考How to put a task to sleep (or delay) in C# 4.0?

public class OperationWithTimeout
{
    public Task<object> Execute(Func<CancellationToken, object> operation, TimeSpan timeout)
    {
        var cancellationToken = new CancellationTokenSource();

        // Two tasks are created. 
        // One which starts the requested operation and second which starts Timer. 
        // Timer is set to AutoReset = false so it runs only once after given 'delayTime'. 
        // When this 'delayTime' has elapsed then TaskCompletionSource.TrySetResult() method is executed. 
        // This method attempts to transition the 'delayTask' into the RanToCompletion state.
        Task<object> operationTask = Task<object>.Factory.StartNew(() => operation(cancellationToken.Token), cancellationToken.Token);
        Task delayTask = Delay(timeout.TotalMilliseconds);

        // Then WaitAny() waits for any of the provided task objects to complete execution.
        Task[] tasks = new Task[]{operationTask, delayTask};
        Task.WaitAny(tasks);

        try
        {
            if (!operationTask.IsCompleted)
            {
                // If operation task didn't finish within given timeout call Cancel() on token and throw 'TimeoutException' exception.
                // If Cancel() was called then in the operation itself the property 'IsCancellationRequested' will be equal to 'true'.
                cancellationToken.Cancel();
                throw new TimeoutException("Timeout waiting for method after " + timeout + ". Method was to slow :-)");
            }
        }
        finally
        {
            cancellationToken.Dispose();
        }

        return operationTask;
    }

    public static Task Delay(double delayTime)
    {
        var completionSource = new TaskCompletionSource<bool>();
        Timer timer = new Timer();
        timer.Elapsed += (obj, args) => completionSource.TrySetResult(true);
        timer.Interval = delayTime;
        timer.AutoReset = false;
        timer.Start();
        return completionSource.Task;
    }
}

如何在控制台应用程序中使用它。
    public static void Main(string[] args)
    {
        var operationWithTimeout = new OperationWithTimeout();
        TimeSpan timeout = TimeSpan.FromMilliseconds(10000);

        Func<CancellationToken, object> operation = token =>
        {
            Thread.Sleep(9000); // 12000

            if (token.IsCancellationRequested)
            {
                Console.Write("Operation was cancelled.");
                return null;
            }

            return 123456;
        };

        try
        {
            var t = operationWithTimeout.Execute(operation, timeout);
            var result = t.Result;
            Console.WriteLine("Operation returned '" + result + "'");
        }
        catch (TimeoutException tex)
        {
            Console.WriteLine(tex.Message);
        }

        Console.WriteLine("Press enter to exit");
        Console.ReadLine();
    }

2
为了详细说明Timothy Shields的干净解决方案:
        if (task == await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(3))))
        {
            return await task;
        }
        else
            throw new TimeoutException();

我发现这个解决方案也适用于任务有返回值的情况,即:

async Task<T>

这里还有更多相关内容:MSDN:编写一个Task.TimeoutAfter方法


1

Jasper的答案帮助了我很多,但我特别想要一个void函数来调用一个带有超时的非任务同步方法。这是我最终得到的代码:

public static void RunWithTimeout(Action action, TimeSpan timeout)
{
    var task = Task.Run(action);
    try
    {
        var success = task.Wait(timeout);
        if (!success)
        {
            throw new TimeoutException();
        }
    }
    catch (AggregateException ex)
    {
        throw ex.InnerException;
    }
}

像这样调用:

RunWithTimeout(() => File.Copy(..), TimeSpan.FromSeconds(3));

1
对于其他人阅读此内容,请注意 'throw ex.InnerException;' 将破坏该异常的堆栈跟踪。 - Adam Caviness
@AdamCaviness 请问您能否澄清一下您的意思?我猜测 AggregateException 是为了处理主任务抛出异常的情况(如果我理解有误,请纠正我)。如果是这样,正确的处理方式是什么呢?谢谢。 - Avrohom Yisroel
1
@AvrohomYisroel 你应该使用 ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); - Adam Caviness

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接