【问题标题】:Monitoring a synchronous method for timeout监视同步方法的超时
【发布时间】:2013-09-06 23:56:00
【问题描述】:

如果同步方法执行时间过长,我正在寻找一种有效的方法来引发超时异常。我看过一些样本,但没有一个完全符合我的要求。

我需要做的是

  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 吗?
  • Robert:谢谢,我关心的是 Thread.Abort()。我不要那样做。似乎太激烈了。就我而言,我不需要中止。
  • @Andre:你应该更喜欢Task.Run 而不是TaskFactory.StartNew。请参阅Stephen Toub's blog 和(如果这还不够令人信服)my blog
  • @Stephen:好点子。谢谢。我将更新示例

标签: c# asynchronous


【解决方案1】:

如果您有一个名为taskTask,您可以这样做:

var delay = Task.Delay(TimeSpan.FromSeconds(3));
var timeoutTask = Task.WhenAny(task, delay);

如果timeoutTask.Result 最终成为task,那么它不会超时。否则,它是delay,它确实超时了。

我不知道这是否会与您实现的行为相同,但这是执行此操作的内置方式。

【讨论】:

  • 谢谢,看起来干净多了。我将看看如何使用它来获得类似的行为,如果一切顺利,我会接受答案
【解决方案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();
    }

【讨论】:

    【解决方案3】:

    详细说明 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

    【讨论】:

      【解决方案4】:

      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));
      

      【讨论】:

      • 对于阅读本文的其他人,请注意 'throw ex.InnerException; ' 将破坏该异常的堆栈跟踪。
      • @AdamCaviness 请您澄清一下您的意思吗?我认为AggregateException 是为了以防主要任务引发异常(但如果我错了,请纠正我)。如果是这样,处理这个问题的正确方法是什么?谢谢
      • @AvrohomYisroel 你应该使用ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-29
      • 1970-01-01
      • 2018-05-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多