Mår*_*röm 72 c# deadlock asynchronous stackexchange.redis
我在调用StackExchange.Redis时遇到了死锁情况.
我不确切知道发生了什么,这是非常令人沮丧的,我希望任何有助于解决或解决此问题的输入.
万一你也有这个问题,不想读这一切; 我建议你将尝试设置
PreserveAsyncOrder到false.Run Code Online (Sandbox Code Playgroud)ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;这样做可能会解决此Q&A所涉及的僵局,并且还可以提高性能.
await和Wait()/ Result).当应用程序/服务启动时,它会正常运行一段时间,然后突然(几乎)所有传入的请求都会停止运行,它们永远不会产生响应.所有这些请求都在等待Redis完成呼叫的死锁.
有趣的是,一旦发生死锁,对Redis的任何调用都将挂起,但前提是这些调用是来自传入的API请求,这些调用是在线程池上运行的.
我们还从低优先级后台线程调用Redis,这些调用即使在发生死锁后也会继续运行.
似乎只有在线程池线程上调用Redis时才会出现死锁.我不再认为这是因为这些调用是在线程池线程上进行的.相反,似乎任何异步Redis调用没有延续,或者同步安全延续,即使在发生死锁情况后也会继续工作.(见下面我认为发生的事情)
混合引起的死锁await和Task.Result(像我们一样同步异步).但是我们的代码在没有同步上下文的情况下运行,因此这里不适用,对吧?
是的,我们不应该这样做.但我们这样做了,我们将不得不继续这样做一段时间.需要迁移到异步世界的大量代码.
同样,我们没有同步上下文,所以这不应该导致死锁,对吧?
ConfigureAwait(false)在任何设置之前设置await对此没有影响.
异步命令和Task.WhenAny等待StackExchange.Redis之后的超时异常
这是线程劫持问题.目前的情况如何?这可能是问题吗?
来自Marc的回答:
...混合等待和等待不是一个好主意.除了死锁之外,这是"同步异步" - 一种反模式.
但他也说:
SE.Redis在内部绕过sync-context(库代码正常),所以它不应该有死锁
因此,根据我的理解,StackExchange.Redis应该与我们是否使用同步异步反模式无关.它只是不推荐,因为它可能是其他代码中死锁的原因.
但是,在这种情况下,据我所知,死锁实际上是在StackExchange.Redis中.如果我错了,请纠正我.
我发现僵局似乎有其源ProcessAsyncCompletionQueue上的124线CompletionManager.cs.
该代码的片段:
while (Interlocked.CompareExchange(ref activeAsyncWorkerThread, currentThread, 0) != 0)
{
// if we don't win the lock, check whether there is still work; if there is we
// need to retry to prevent a nasty race condition
lock(asyncCompletionQueue)
{
if (asyncCompletionQueue.Count == 0) return; // another thread drained it; can exit
}
Thread.Sleep(1);
}
Run Code Online (Sandbox Code Playgroud)
我发现在僵局期间; activeAsyncWorkerThread是我们正在等待Redis调用完成的线程之一.(我们的线程 =运行我们代码的线程池线程).所以上面的循环被认为是永远的.
不知道细节,这肯定是错的; StackExchange.Redis正在等待它认为是活动的异步工作线程的线程,而它实际上是一个与之完全相反的线程.
我想知道这是否是由于线程劫持问题(我不完全理解)?
我想弄清楚的两个主要问题:
即使在没有同步上下文的情况下运行,也可能混淆await和Wait()/ Result或导致死锁?
我们是否遇到StackExchange.Redis中的错误/限制?
从我的调试结果来看,问题似乎是:
next.TryComplete(true);
Run Code Online (Sandbox Code Playgroud)
...在第162行CompletionManager.cs可能在某些情况下让当前线程(它是活动的异步工作线程)徘徊并开始处理其他代码,可能导致死锁.
在不知道细节并只考虑这个"事实"的情况下,在调用期间临时释放活动的异步工作线程似乎是合乎逻辑的TryComplete.
我想像这样的东西可以工作:
// release the "active thread lock" while invoking the completion action
Interlocked.CompareExchange(ref activeAsyncWorkerThread, 0, currentThread);
try
{
next.TryComplete(true);
Interlocked.Increment(ref completedAsync);
}
finally
{
// try to re-take the "active thread lock" again
if (Interlocked.CompareExchange(ref activeAsyncWorkerThread, currentThread, 0) != 0)
{
break; // someone else took over
}
}
Run Code Online (Sandbox Code Playgroud)
我想我最大的希望是Marc Gravell会读到这个并提供一些反馈:-)
我在上面写过,我们的代码不使用同步上下文.这只是部分正确:代码作为控制台应用程序或Azure辅助角色运行.在这些环境SynchronizationContext.Current中null,这就是为什么我写道我们在没有同步上下文的情况下运行.
但是,在阅读了It的所有关于SynchronizationContext之后我才知道事实并非如此:
按照惯例,如果线程的当前SynchronizationContext为null,则它隐式具有默认的SynchronizationContext.
默认同步上下文不应该是死锁的原因,因为基于UI(WinForms,WPF)同步上下文可能 - 因为它不暗示线程关联.
消息完成后,将检查其完成源是否被认为是同步安全的.如果是,则完成操作是内联执行的,一切都很好.
如果不是,那么想法是在新分配的线程池线程上执行完成操作.这也是工作的时候就好了ConnectionMultiplexer.PreserveAsyncOrder是false.
但是,当ConnectionMultiplexer.PreserveAsyncOrder是true(默认值)时,那些线程池线程将使用完成队列序列化它们的工作,并确保它们中的任何一个在任何时候都是活动的异步工作线程.
当一个线程成为活动的异步工作线程时,它将一直是那个,直到它耗尽了完成队列.
问题是完成操作不是同步安全的(从上面开始),它仍然在不能被阻止的线程上执行,因为这将阻止其他非同步安全消息的完成.
请注意,即使活动异步工作线程被阻止,使用同步安全的完成操作完成的其他消息也将继续正常工作.
我建议的"修复"(上面)不会以这种方式导致死锁,但它会混淆保留异步完成顺序的概念.
因此,这里可能得出的结论是,无论是否在没有同步上下文的情况下运行,与/ 何时混合都是不安全的awaitResultWait()PreserveAsyncOrdertrue?
(至少在我们可以使用.NET 4.6和新版本之前TaskCreationOptions.RunContinuationsAsynchronously,我想)
Mår*_*röm 21
这些是我发现这个死锁问题的解决方法:
默认情况下,StackExchange.Redis将确保以与接收结果消息相同的顺序完成命令.这可能会导致此问题中描述的死锁.
通过设置PreserveAsyncOrder为禁用该行为false.
ConnectionMultiplexer connection = ...;
connection.PreserveAsyncOrder = false;
Run Code Online (Sandbox Code Playgroud)
这样可以避免死锁,也可以提高性能.
我鼓励任何遇到死锁问题的人尝试这种解决方法,因为它非常干净和简单.
您将放弃以与底层Redis操作完成相同的顺序调用异步延续的保证.但是,我真的不明白为什么你会依赖它.
当StackExchange.Redis中的活动异步工作线程完成命令并且内联执行完成任务时,会发生死锁.
可以通过使用自定义来防止任务内联执行TaskScheduler并确保TryExecuteTaskInline返回false.
public class MyScheduler : TaskScheduler
{
public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return false; // Never allow inlining.
}
// TODO: Rest of TaskScheduler implementation goes here...
}
Run Code Online (Sandbox Code Playgroud)
实现良好的任务调度程序可能是一项复杂的任务.但是,您可以使用ParallelExtensionExtras库(NuGet包)中的现有实现或从中获取灵感.
如果您的任务调度程序将使用自己的线程(而不是来自线程池),那么除非当前线程来自线程池,否则允许内联可能是个好主意.这将起作用,因为StackExchange.Redis中的活动异步工作线程始终是线程池线程.
public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
// Don't allow inlining on a thread pool thread.
return !Thread.CurrentThread.IsThreadPoolThread && this.TryExecuteTask(task);
}
Run Code Online (Sandbox Code Playgroud)
另一个想法是使用线程本地存储将调度程序附加到其所有线程.
private static ThreadLocal<TaskScheduler> __attachedScheduler
= new ThreadLocal<TaskScheduler>();
Run Code Online (Sandbox Code Playgroud)
确保在线程开始运行时分配此字段并在完成时清除:
private void ThreadProc()
{
// Attach scheduler to thread
__attachedScheduler.Value = this;
try
{
// TODO: Actual thread proc goes here...
}
finally
{
// Detach scheduler from thread
__attachedScheduler.Value = null;
}
}
Run Code Online (Sandbox Code Playgroud)
然后,只要在自定义调度程序"拥有"的线程上完成任务,就可以允许内联任务:
public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
// Allow inlining on our own threads.
return __attachedScheduler.Value == this && this.TryExecuteTask(task);
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
8005 次 |
| 最近记录: |