我有一个方法定义为:
public Task<ReturnsMessage> Function()
{
var task = Task.Run(() =>
{
var result = SyncMethod();
return new ReturnMessage(result);
});
if (task.Wait(delay))
{
return task;
}
var tcs = new TaskCompletionSource<ReturnMessage>();
tcs.SetCanceled();
return tcs.Task;
}
现在,基于maxAttempts值在循环中调用它:
(方法名称可重试调用)
for (var i = 0; i < maxAttempts; i++)
{
try
{
return Function().Result;
}
catch (Exception e)
{
}
}
它工作得非常好,但是当有大量负载时,我发现线程急剧增加,dump向我显示以下警告:
谁能建议我处理这种情况的最佳方法,这样我就不会看到任何类型的死锁?
2条答案
按热度按时间dsekswqp1#
您正在死锁应用程序,因为您没有使用
async
/await
或ConfigureAwait(false
,而是选择使用Task.Wait
和Task.Result
。首先,您应该知道
Task.Run
捕获执行它的线程的SynchronizationContext
(调用者线程或父线程)。然后Task.Run
在新的ThreadPool
线程上执行委托。当完成时,它将返回到父线程以继续执行剩余代码。当返回时,它将返回到所捕获的SyncronizationContext
(SyncronizationContext
仅被捕获用于延续)。使用
Task.Wait
和Task.Result
违反了概念。两个Task
成员都将 * 同步 * 调用Task
,这意味着:a)父线程将阻塞自己,直到
Task
完成。它处于 * busy waiting * 状态,这意味着它除了等待之外不能做任何事情。如果调用是使用async/await * 异步 * 的,那么父线程将继续执行其他操作,而不是等待(例如,在UI线程的情况下的UI相关操作),直到Task
发信号通知其完成)。b)
Task
完成了,但是不能返回到捕获的SynchronizationContext
来执行剩余的代码(Task.Wait
之后的代码),因为父线程仍然忙于等待任务运行完成。现在Task
必须等待父线程准备就绪(完成等待)以执行继续。这是最终锁定父线程的互斥情况:父线程忙于等待
Task
,而Task
忙于等待父线程。两者都无限期地等待对方。这就是死锁。如果父线程是主线程,则整个应用程序死锁并挂起。由于您在一个地方使用了
Task.Wait
,而在另一个地方使用了Task.Result
,因此造成了两种潜在的死锁情况:让我们逐步了解
Function()
代码:1.创建任务并启动它:
变量任务=任务.运行(()=〉{变量结果=同步方法();返回新的返回消息(结果); });
Task.Run
捕获当前SynchronizationContext
并在后台开始执行lambda表达式,同时父线程继续执行下一条指令(if
-语句)。如果此处使用了await Task.Run()
,则跟随在await
之后的剩余代码将被排队到后续队列中以供稍后执行)。这样做的目的是让当前线程可以暂时返回(离开当前上下文),这样它就不需要等待和阻塞(使用Task.Wait
)。一旦子线程运行完成,剩余的代码将由Task
执行,因为它之前在继续队列中排队。task.Wait()
等待,直到后台线程完成。等待意味着阻止线程继续执行。调用堆栈已停止。这相当于后台线程的同步,因为父线程不再继续执行,而是阻止://执行
task
对象超时的危险实现if(task. Wait(delay)){return task;}task.Wait()
阻塞等待子线程完成的当前线程(SynchronizationContext
)。子任务完成后,Task
尝试执行捕获的SynchronizationContext
中的继续队列中先前排队的剩余代码。但此上下文被等待子任务完成的线程阻塞。潜在死锁情况一。以下剩余代码将无法访问:
引入
async
和await
是为了消除阻塞等待。await
允许父线程返回并继续。await
之后的剩余代码将作为捕获的SynchronizationContext
中的延续执行。这也是使用
Task.WhenAny
(不首选)的正确任务超时解决方案对第一个死锁的修复:或者使用
CancellationTokenSouce
timeout构造函数重载(首选),使用更好的替代超时方法修复第一个死锁:第二个潜在的死锁代码是
Function()
的消耗:来自Microsoft文档:
访问属性的[
Task.Result
] get访问器将阻塞调用线程,直到异步操作完成; * * 它等效于调用Wait方法**。死锁的原因与前面解释的相同:阻止所调度的继续的执行的被阻止的
SynchronizationContext
。要修复第二个死锁,我们可以使用 * async/await *(首选)或
ConfigreAwait(false)
:或
ConfigreAwait(false)
。此方法可用于强制同步执行异步方法:ConfigreAwait(false)
指示Task
忽略捕获的SynchronizationContext
,并在永远不会成为父线程的另一个ThreadPool
线程上继续执行继续队列。00jrzges2#
您使用Task.Run启动任务,如果任务超时,则返回取消,但您从未停止任务。它们只是继续在后台运行。
你的代码应该是async/await,使用CancellationSource,并在SyncMethod()中处理取消标记,但是如果你不能,并且你想异步运行一个方法,并在一段时间后强制终止它,你可能应该使用Threads和Abort threads。
警告:中止线程是不安全的,除非你知道你在做什么,它甚至可能从.NET的未来版本中删除。
其实我之前研究过这个问题:https://siderite.dev/blog/how-to-timeout-task-and-make-sure-it.html