![](/img/trans.png)
[英]C#/Tasks Error being logged twice with Task.Wait after wait 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.Wait
和Task.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.WhenAny
和Task.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.