【问题标题】:Running two tasks with Parallel.Invoke and add a timeout in case one task takes longer使用 Parallel.Invoke 运行两个任务并添加超时以防一个任务需要更长的时间
【发布时间】:2020-08-20 09:21:29
【问题描述】:

我正在调用两个依赖于一些外部 Web 服务的函数。目前,它们并行运行,当它们都完成时,执行恢复。但是,如果外部服务器花费太多时间来处理请求,它可能会锁定我的代码一段时间。

我想添加一个超时,这样如果服务器响应时间超过 10 秒,那么就继续。这就是我所拥有的,我该如何添加超时?

Parallel.Invoke(

    () => FunctionThatCallsServer1(TheParameter),
    () => FunctionThatCallsServer2(TheParameter)            
);

RunThisFunctionNoMatterWhatAfter10Seconds();

【问题讨论】:

  • 那些外部调用可以取消吗?如果是这样,您可以使用 CancellationTokenSource 创建一个令牌,它将在 X 时间后自动取消并传递它。

标签: c# task


【解决方案1】:

您需要创建一个CancellationTokenSource 的实例,并且在创建时您可以配置超时时间,例如

var cts = new CancellationTokenSource(timeout);

然后您需要创建一个ParallelOptions 的实例,在其中将ParallelOptions.CancellationToken 设置为CancellationTokenSource 的令牌,例如

var options = new ParallelOptions {
    CancellationToken = cts.Token,
};

然后您可以使用选项和您的操作致电Parallel.Invoke

try 
{
    Parallel.Invoke(
        options,
        () => FunctionThatCallsServer1(token),
        () => FunctionThatCallsServer2(token)            
    );
}
catch (OperationCanceledException ex)
{
    // timeout reached
    Console.WriteLine("Timeout");
    throw;
}

但您还需要将令牌交给被调用的服务器函数并处理这些操作中的超时。

这是因为Parallel.Invoke 只会在它获得的令牌被取消的情况下开始操作之前检查。这意味着如果所有操作在超时发生之前启动,只要操作需要完成,Parallel.Invoke 调用就会阻塞。

更新:

测试取消的一个好方法是定义FunctionThatCallsServer1like,

static void FunctionThatCallsServer1(CancellationToken token) {
    var endTime = DateTime.Now.AddSeconds(5);

    while (DateTime.Now < endTime) {
        token.ThrowIfCancellationRequested();

        Thread.Sleep(1);
    }
}

【讨论】:

  • 我试过这个。在其中一个函数上,我添加了 30 秒的 Thread.Sleep 并将 Parrallel.Invoke 的令牌超时设置为 10 秒:代码只是等待 Thread.Sleep 恢复,而不是按预期提前取消。
  • 如果您只是在Parallel.Invoke 调用的操作中执行Thread.Sleep,那么它们在启动后就没有机会被取消。 Parallel.Invoke 无法阻止线程执行其操作之一。该操作需要自己处理取消状态。
【解决方案2】:

我不认为有一种简单的方法可以让 Parallel.Invoke 在函数启动后超时,显然它们会在十秒后完成。即使您取消,Parallel.Invoke 也会等待函数完成,因此您必须想办法尽早完成函数。

但是,Parallel.Invoke 在幕后使用任务,如果您直接使用任务而不是 Parallel.Invoke,则可以提供超时。下面的代码显示了如何:

Task task1 = Task.Run(() => FunctionThatCallsServer1(TheParameter));
Task task2 = Task.Run(() => FunctionThatCallsServer2(TheParameter));
// 10000 is timeout in ms, allTasksCompleted is true if they completed, false if timed out
bool allTasksCompleted = Task.WaitAll(new[] { task1, task2 }, 10000);
RunThisFunctionNoMatterWhatAfter10Seconds();

此代码与 Parallel.Invoke 的一个细微差别是,如果您有大量函数,则 Parallel.Invoke 将更好地管理任务创建,而不是像这里那样盲目地为每个函数创建一个任务。 Parallel.Invoke 将创建有限数量的任务并在功能完成时重新使用它们。只需如上所述调用几个函数,这将不是问题。

【讨论】:

    【解决方案3】:

    下面是代码:

    using System;
    using System.Threading.Tasks;
    namespace Algorithums
    {
    public class Program
    {
        public static void Main(string[] args)
        {
            ParelleTasks();
            Console.WriteLine("Main");
            Console.ReadLine();
        }
    
        private static void ParelleTasks()
        {
            Task t = Task.Run(() => {
                FunctionThatCallsServers();
                Console.WriteLine("Task ended after 20 Seconds");
            });
    
            try
            {
                Console.WriteLine("About to wait for 10 sec completion of task {0}", t.Id);
                bool result = t.Wait(10000);
                Console.WriteLine("Wait completed normally: {0}", result);
                Console.WriteLine("The task status:  {0:G}", t.Status);
            }
            catch (OperationCanceledException e)
            {
                Console.WriteLine("Error: " + e.ToString());
               
            }
    
            RunThisFunctionNoMatterWhatAfter10Seconds();
        }
    
    
        private static bool FunctionThatCallsServers()
        {
            Parallel.Invoke(
                             () => FunctionThatCallsServer1(),
                             () => FunctionThatCallsServer2()
                        );
            return true;
        }
    
        private static void FunctionThatCallsServer1()
        {
            System.Threading.Thread.Sleep(20000);
            Console.WriteLine("FunctionThatCallsServer1");
        }
    
        private static void FunctionThatCallsServer2()
        {
            System.Threading.Thread.Sleep(20000);
            Console.WriteLine("FunctionThatCallsServer2");
        }
    
        private static void RunThisFunctionNoMatterWhatAfter10Seconds()
        {
            Console.WriteLine("RunThisFunctionNoMatterWhatAfter10Seconds");
        }
    }
    
    
    }
    

    【讨论】:

    • 首先:没有解释!第二:不要使用Task 来执行此操作,因为Tasks 是为异步操作而非并行设计的。这个问题可以而且应该用Parallel.Invoke()来解决
    • 这种情况下不使用异步操作的原因是什么?这会影响性能还是什么?
    • 首先:当然会有性能损失,这是因为任务总是伴随着计算开销。第二:您的特定解决方案将阻止两个线程而不是一个。对Parallel.Invoke 的调用将阻塞一个线程,对t.Wait(10000) 的调用将阻塞另一个线程,因为Task.Run 将在某个线程池线程上执行给定的操作/函数。
    • 有道理。感谢您的反馈。
    猜你喜欢
    • 2017-10-25
    • 2021-10-15
    • 2021-05-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-28
    相关资源
    最近更新 更多