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 个)时,输出非常非常
接近 1000,缺失大约 10 到 50 毫秒,这是 100% 可以接受的。
但是,如果我 运行 以并发方式进行此操作,假设一次有 5 个请求,它开始下滑,达到 100 毫秒...如果我 运行 以 25 个并发请求进行此操作,我开始看到以秒为单位的滑动,当我 运行 高于 35 时,滑动优于 10 秒(此时我将睡眠时间增加到 60 秒,因为服务在框架可以 "notice" 超时之前返回)
谁能告诉我这是怎么回事?为什么 "slip" 会发展到如此程度?对于我想要实现的目标,是否有更可靠的实现?
详情:
服务很简单:
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 个线程,线程以循环方式调度,每个线程的执行时间仅为 2 毫秒。假设如果所有 10 个线程都在第 0 毫秒启动并且 StopWatch
的 20 毫秒已经过去,每个线程只有 4 毫秒的执行时间,剩下的 16 毫秒,每个线程将等待轮到它执行.因此,如果您使用特定超时值将特定线程阻塞一定数量的 ms,这并不意味着线程执行将在指定时间内完成。线程完成执行的实际时间,包括线程等待获取下一个执行周期的时间。
因此,当您调用 task.Wait(1000)
时,线程将被阻塞其执行时间的 1000 毫秒,但秒表运行时间不会被阻塞
正如其他人在评论中所解释的那样,原始 async
代码片段被 Task.Wait
和 Task.Result
的用法阻塞。这 can cause issues 不应该这样做。
使用 Task.Run
导致执行时间增加,正如您看到的 threadpool was becoming exhausted 随着顺序调用的数量增加。
如果你想运行一个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
结合到 运行 超时任务以及传入的任务参数。如果传入的任务在超时之前完成,结果将为 return编辑。但是,如果超时先完成,(hasValue: false, value: default)
将被 returned。
你的例子的用法:
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 毫秒延迟。当任务在超时期限内完成时,此方法将立即return。
我正在尝试为不支持 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 个)时,输出非常非常
接近 1000,缺失大约 10 到 50 毫秒,这是 100% 可以接受的。
但是,如果我 运行 以并发方式进行此操作,假设一次有 5 个请求,它开始下滑,达到 100 毫秒...如果我 运行 以 25 个并发请求进行此操作,我开始看到以秒为单位的滑动,当我 运行 高于 35 时,滑动优于 10 秒(此时我将睡眠时间增加到 60 秒,因为服务在框架可以 "notice" 超时之前返回)
谁能告诉我这是怎么回事?为什么 "slip" 会发展到如此程度?对于我想要实现的目标,是否有更可靠的实现?
详情:
服务很简单:
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 个线程,线程以循环方式调度,每个线程的执行时间仅为 2 毫秒。假设如果所有 10 个线程都在第 0 毫秒启动并且 StopWatch
的 20 毫秒已经过去,每个线程只有 4 毫秒的执行时间,剩下的 16 毫秒,每个线程将等待轮到它执行.因此,如果您使用特定超时值将特定线程阻塞一定数量的 ms,这并不意味着线程执行将在指定时间内完成。线程完成执行的实际时间,包括线程等待获取下一个执行周期的时间。
因此,当您调用 task.Wait(1000)
时,线程将被阻塞其执行时间的 1000 毫秒,但秒表运行时间不会被阻塞
正如其他人在评论中所解释的那样,原始 async
代码片段被 Task.Wait
和 Task.Result
的用法阻塞。这 can cause issues 不应该这样做。
使用 Task.Run
导致执行时间增加,正如您看到的 threadpool was becoming exhausted 随着顺序调用的数量增加。
如果你想运行一个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
结合到 运行 超时任务以及传入的任务参数。如果传入的任务在超时之前完成,结果将为 return编辑。但是,如果超时先完成,(hasValue: false, value: default)
将被 returned。
你的例子的用法:
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 毫秒延迟。当任务在超时期限内完成时,此方法将立即return。