监视超时的同步方法

时间:2013-09-03 16:49:33

标签: c# asynchronous

如果同步方法执行时间太长,我正在寻找一种有效的方法来抛出超时异常。我见过一些样品,但没有什么能做到我想要的。

我需要做的是

  1. 检查同步方法是否超过其SLA
  2. 如果确实抛出超时异常
  3. 如果执行时间太长,必须终止同步方法。 (多次故障会使断路器跳闸并防止级联故障)

    到目前为止我的解决方案如下所示。请注意,我确实将CancellationToken传递给了sync方法,希望它能在超时时遵循取消请求。此外,我的解决方案返回一个任务,然后我可以根据需要等待我的调用代码。

    我担心的是,这个代码会为每个监控方法创建两个任务。我认为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;
      }
    

4 个答案:

答案 0 :(得分:18)

如果您有一个名为Task的{​​{1}},则可以执行以下操作:

task

如果var delay = Task.Delay(TimeSpan.FromSeconds(3)); var timeoutTask = Task.WhenAny(task, delay); 最终成为timeoutTask.Result,那么它就不会超时。否则,它是task并且它确实超时。

我不知道这是否会与您实施的内容完全相同,但这是内置的方法。

答案 1 :(得分:2)

我已为.NET 4.0重新编写了此解决方案,其中某些方法不可用,例如Delay。此版本正在监视返回object的方法。如何在Delay中实施.NET 4.0来自此处: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 :(得分:2)

详细阐述Timothy Shields的清洁解决方案:

        if (task == await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(3))))
        {
            return await task;
        }
        else
            throw new TimeoutException();

我发现,这个解决方案也将处理Task有一个返回值的情况 - 即:

async Task<T>

更多信息请点击此处:MSDN: Crafting a Task.TimeoutAfter Method

答案 3 :(得分:0)

贾斯帕的回答让我大部分时间,但我特别想要一个 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));