繁体   English   中英

C# 核心 - Task.Wait(int Timeout) 未按预期等待

[英]C# Core - Task.Wait(int Timeout) not awaiting as expected

我正在尝试为不支持 CancellationTokens 或预定义超时的 WCF 调用实现超时机制。 为了做到这一点,我创建了一个小项目并提出了这个结构:

       var st = Stopwatch.StartNew();

       try
       {
           var responseTask = Task.Run(() =>
           {
               var task = new WaitingService.ServiceClient().GetDataAsync(60000);
               if (!task.Wait(1000))
               {
                   return null;
               }
               return task.Result;
           });
           await responseTask;
       }
       catch { }
       return Ok("Awaited for " + st.ElapsedMilliseconds + "ms. Supposed to be " + sleep);

当我在本地机器上按顺序运行此代码(一次调用 1 个)时,output 非常接近 1000,丢失了大约 10 到 50 毫秒,这是 100% 可以接受的。
但是如果我以并发方式运行它,假设一次 5 个请求,它开始滑到 100 毫秒......如果我在 25 个并发请求上运行它,我开始在几秒钟内看到滑移,当我运行高于 35 的滑动优于 10 秒(此时我将睡眠时间增加到 60 秒,因为服务在框架“注意到”它超时之前返回)

谁能告诉我发生了什么事? 为什么这种“滑倒”会发生到这样的程度? 对于我想要实现的目标,是否有更可靠的实现?

细节:
服务非常简单:

public string GetData(int value)
{
    Console.WriteLine("Will sleep for " + value);
    Thread.Sleep(value);
    return string.Format("Slept for: {0}ms", value);
}

编辑 1
我还测试了这种情况:

       var st = Stopwatch.StartNew();
       CancellationTokenSource src = new CancellationTokenSource(1000);

       try
       {
           var responseTask = Task.Run(() =>
           {
               var task = new WaitingService.ServiceClient().GetDataAsync(sleep);
               if (!task.Wait(1000,src.Token))
               {
                   return null;
               }
               task.Wait(src.Token);
               return task.Result;
           });
           await responseTask;
       }
       catch { }
       return Ok("Awaited for " + st.ElapsedMilliseconds + "ms. Supposed to be " + sleep);

但实际上我得到了更糟糕的结果......滑倒加剧了......

编辑 2
以下实现获得了更好的结果,50 个并发请求很少超过 2 秒!

var st = Stopwatch.StartNew();

try
{
    var responseTask = Task.Run(async () =>
    {
        var task = new WaitingService.ServiceClient().GetDataAsync(sleep);
        do
        {
            await Task.Delay(50);
        }while (task.Status == TaskStatus.Running || st.ElapsedMilliseconds < 1000);
        if (task.Status == TaskStatus.RanToCompletion)
        {
            return task.Result;
        }
        else { return null; }
    });
    await responseTask;
}
catch { }
return Ok("Awaited for " + st.ElapsedMilliseconds + "ms. Supposed to be " + sleep);

假设您的进程中有 10 个线程,并且线程以循环方式调度,其中每个线程仅获得 2ms 的执行时间。 假设如果所有 10 个线程都在第 0 毫秒启动并且秒表已经过去 20 毫秒,则每个线程仅获得 4 毫秒的执行时间, StopWatch为 16 毫秒,每个线程将等待轮到它执行。 因此,如果您使用特定超时值阻塞特定线程一定数量的毫秒,这并不意味着线程执行将在指定时间内完成。 线程完成执行的实际时间,包括线程等待获取下一个执行周期的时间。

因此,当您调用task.Wait(1000)时,线程将被阻塞 1000 毫秒的执行时间,而不是秒表经过的时间

正如其他人在评论中解释的那样,原始的async代码片段因使用Task.WaitTask.Result而被阻塞。 可能会导致问题,不应该这样做。

使用Task.Run会导致执行时间增加,您看到的线程池随着顺序调用数量的增加而耗尽

如果您想在不阻塞的情况下运行超时Task ,您可以使用以下方法:

public static async Task<(bool hasValue, TResult value)> WithTimeout<TResult>(Task<TResult> task, int msTimeout)
{
    using var timeoutCts = new CancellationTokenSource();

    var timeoutTask = Task.Delay(msTimeout, timeoutCts.Token);
    var completedTask = await Task.WhenAny(task, timeoutTask);

    if (completedTask == task)
    {
        timeoutCts.Cancel(); //Cancel timeoutTask
        return (true, await task); //Get result or propagate exception
    }

    //completedTask was our timeoutTask
    return (false, default);
}

该方法结合Task.WhenAnyTask.Delay在传入的任务参数旁边运行超时任务。如果传入的任务在超时之前完成,将返回结果。 但是,如果超时首先完成,则返回(hasValue: false, value: default)

与您的示例一起使用:

var task = new WaitingService.ServiceClient().GetDataAsync(sleep);
var result = await WithTimeout(task, 1000);

if (result.hasValue)
{
   //Do something with result.value;
}

请务必注意,由于您的任务不支持取消,它将继续运行。 除非它在其他地方处理,否则它可能会抛出未被捕获的异常。

因为我们正在通过非阻塞Task.WhenAny等待任务,所以您不应该像在原始示例中看到的那样耗尽线程池。

在您的最后一个示例中,在继续执行结果之前可能会出现不必要的额外 0-50 毫秒延迟。 当任务在超时期限内完成时,此方法将立即返回。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM