访问 StackExchange.Redis 时死锁
Deadlock when accessing StackExchange.Redis
我在调用 StackExchange.Redis 时 运行 陷入僵局。
我不知道到底发生了什么,这非常令人沮丧,如果有任何意见可以帮助解决或解决此问题,我将不胜感激。
In case you have this problem too and don't want to read all this;
I suggest that you'll try setting PreserveAsyncOrder
to false
.
ConnectionMultiplexer connection = ...;
connection.PreserveAsyncOrder = false;
Doing so will probably resolve the kind of deadlock that this Q&A is about and could also improve performance.
我们的设置
- 代码是 运行 作为控制台应用程序或作为 Azure 辅助角色。
- 它使用 HttpMessageHandler 公开 REST api,因此入口点是异步的。
- 代码的某些部分具有线程关联性(由单个线程拥有,并且必须 运行 由单个线程拥有)。
- 部分代码是异步的。
- 我们正在做 sync-over-async and async-over-sync 反模式。 (混合
await
和 Wait()
/Result
)。
- 我们只在访问 Redis 时使用异步方法。
- 我们正在为 .NET 4.5 使用 StackExchange.Redis 1.0.450。
死锁
当 application/service 启动时,它 运行 通常会持续一段时间,然后突然(几乎)所有传入的请求都停止运行,它们永远不会产生响应。所有这些请求都陷入僵局,等待对 Redis 的调用完成。
有趣的是,一旦发生死锁,对 Redis 的任何调用都会挂起,但前提是这些调用是从传入的 API 请求发出的,这些请求在线程池上 运行。
我们还从低优先级后台线程调用 Redis,这些调用即使在死锁发生后仍继续运行。
好像只有在线程池线程上调用Redis才会出现死锁。我不再认为这是由于这些调用是在线程池线程上进行的。相反,似乎任何异步 Redis 调用 没有继续,或者 sync safe 继续, 即使在发生死锁情况后也会继续工作. (请参阅下面的我认为会发生什么)
相关
StackExchange.Redis Deadlocking
混合 await
和 Task.Result
造成的死锁(像我们一样同步异步)。但是我们的代码 运行 没有同步上下文,所以这里不适用,对吧?
How to safely mix sync and async code?
是的,我们不应该那样做。但是我们这样做了,而且我们将不得不继续这样做一段时间。大量代码需要迁移到异步世界中。
同样,我们没有同步上下文,所以这不应该导致死锁,对吗?
在任何 await
之前设置 ConfigureAwait(false)
对此没有影响。
Timeout exception after async commands and Task.WhenAny awaits in StackExchange.Redis
这是线程劫持问题。目前这方面的情况如何?这可能是这里的问题吗?
StackExchange.Redis async call hangs
来自马克的回答:
...mixing Wait and await is not a good idea. In addition to deadlocks, this is "sync over async" - an anti-pattern.
但他也说:
SE.Redis bypasses sync-context internally (normal for library code), so it shouldn't have the deadlock
因此,根据我的理解,StackExchange.Redis 应该不知道我们是否在使用 sync-over-async 反模式。只是不推荐,因为它可能是 other 代码中死锁的原因。
然而,在这种情况下,据我所知,死锁确实在 StackExchange.Redis 内部。如有错误请指正
调试结果
我发现死锁似乎源于 line 124 of CompletionManager.cs
上的 ProcessAsyncCompletionQueue
。
该代码片段:
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);
}
我发现在死锁期间; activeAsyncWorkerThread
是我们正在等待 Redis 调用完成的线程之一。 (我们的线程 = 一个线程池线程运行宁我们的代码)。所以上面的循环被认为永远持续下去。
在不知道细节的情况下,这肯定感觉不对; StackExchange.Redis 正在等待它认为是 活动异步工作线程 的线程,而实际上它是一个与此完全相反的线程。
请问是不是线程劫持问题(我不是很懂)?
怎么办?
我想弄清楚的主要两个问题:
混合使用 await
和 Wait()
/Result
是否会导致死锁,即使 运行ning 没有同步上下文?
我们 运行 正在进入 StackExchange.Redis 中的 bug/limitation 吗?
可能的解决方法?
从我的调试结果来看,问题似乎在于:
next.TryComplete(true);
...on line 162 in CompletionManager.cs
在某些情况下可能会让当前线程(即 活动异步工作线程 )离开并开始处理其他代码,可能导致死锁。
不知道细节,只是考虑这个 "fact",那么在 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
}
}
我想我最大的希望是 Marc Gravell 会阅读这篇文章并提供一些反馈:-)
无同步上下文 = 默认同步上下文
我在上面写过我们的代码不使用 synchronization context. This is only partially true: The code is run as either a Console application or as an Azure Worker Role. In these environments SynchronizationContext.Current
是 null
,这就是为什么我写我们 运行ning without 同步上下文。
然而,在阅读 It's All About the SynchronizationContext 之后,我了解到事实并非如此:
By convention, if a thread’s current SynchronizationContext is null, then it implicitly has a default SynchronizationContext.
默认同步上下文不应该是死锁的原因,因为基于 UI 的(WinForms、WPF)同步上下文可能是 - 因为它并不暗示线程关联。
我认为会发生什么
消息完成后,将检查其完成源是否被视为同步安全。如果是,则内联执行完成操作,一切正常。
如果不是,想法是在新分配的线程池线程上执行完成操作。当 ConnectionMultiplexer.PreserveAsyncOrder
是 false
.
时,这也很好用
但是,当 ConnectionMultiplexer.PreserveAsyncOrder
为 true
(默认值)时,这些线程池线程将使用 完成队列 序列化它们的工作,并通过确保其中至多一个是 active async worker thread 在任何时候。
当一个线程成为活动的异步工作线程时,它将继续保持这种状态,直到它耗尽完成队列。
问题是完成操作不同步安全(从上面看),它仍然在不能被阻塞的线程上执行 因为这会阻止其他 非同步安全 消息完成。
请注意,其他通过 同步安全 完成操作完成的消息将继续正常工作,即使 活动异步工作线程 被屏蔽了。
我建议的"fix"(上文)不会以这种方式导致死锁,但是它会混淆保留异步完成顺序的概念。
所以这里可能要得出的结论是 当 PreserveAsyncOrder
是 true
,不管我们是不是运行没有同步上下文?
(至少在我们可以使用 .NET 4.6 和新的 TaskCreationOptions.RunContinuationsAsynchronously
之前,我想 )
我根据上面的详细信息进行了很多猜测,并且不知道您拥有的源代码。听起来您可能在 .Net 中遇到了一些内部的、可配置的限制。你不应该碰到那些,所以我的猜测是你没有处理对象,因为它们在线程之间浮动,这不允许你使用 using 语句来干净地处理它们的对象生命周期。
这详细说明了 HTTP 请求的限制。类似于旧的 WCF 问题,当您不处理连接时,所有 WCF 连接都会失败。
这更像是一种调试帮助,因为我怀疑您是否真的使用了所有 TCP 端口,但是关于如何找到您拥有多少个打开的端口以及到哪里的很好的信息。
https://msdn.microsoft.com/en-us/library/aa560610(v=bts.20).aspx
这些是我发现的解决此死锁问题的解决方法:
解决方法 #1
默认情况下 StackExchange.Redis 将确保命令的完成顺序与结果消息的接收顺序相同。如本问题中所述,这可能会导致死锁。
通过将 PreserveAsyncOrder
设置为 false
来禁用该行为。
ConnectionMultiplexer connection = ...;
connection.PreserveAsyncOrder = false;
这将避免死锁,也可以 improve performance。
我鼓励 运行 遇到死锁问题的任何人尝试此解决方法,因为它非常干净和简单。
您将无法保证异步延续的调用顺序与底层 Redis 操作的完成顺序相同。但是,我真的不明白为什么你会依赖它。
解决方法 #2
当 StackExchange.Redis 中的 活动异步工作线程 完成命令并且内联执行完成任务时发生死锁。
可以通过使用自定义 TaskScheduler
and ensure that TryExecuteTaskInline
returns 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...
}
实施一个好的任务调度程序可能是一项复杂的任务。但是,ParallelExtensionExtras library (NuGet package) 中的现有实现可供您使用或从中汲取灵感。
如果您的任务调度程序将使用自己的线程(而不是来自线程池),那么允许内联可能是个好主意,除非当前线程来自线程池。这将起作用,因为 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);
}
另一个想法是使用 thread-local storage.
将调度程序附加到它的所有线程
private static ThreadLocal<TaskScheduler> __attachedScheduler
= new ThreadLocal<TaskScheduler>();
确保在线程启动时分配此字段 运行ning 并在其完成时清除:
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;
}
}
然后您可以允许内联任务,只要它由自定义调度程序在 "owned" 的线程上完成即可:
public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
// Allow inlining on our own threads.
return __attachedScheduler.Value == this && this.TryExecuteTask(task);
}
我在调用 StackExchange.Redis 时 运行 陷入僵局。
我不知道到底发生了什么,这非常令人沮丧,如果有任何意见可以帮助解决或解决此问题,我将不胜感激。
In case you have this problem too and don't want to read all this; I suggest that you'll try setting
PreserveAsyncOrder
tofalse
.ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;
Doing so will probably resolve the kind of deadlock that this Q&A is about and could also improve performance.
我们的设置
- 代码是 运行 作为控制台应用程序或作为 Azure 辅助角色。
- 它使用 HttpMessageHandler 公开 REST api,因此入口点是异步的。
- 代码的某些部分具有线程关联性(由单个线程拥有,并且必须 运行 由单个线程拥有)。
- 部分代码是异步的。
- 我们正在做 sync-over-async and async-over-sync 反模式。 (混合
await
和Wait()
/Result
)。 - 我们只在访问 Redis 时使用异步方法。
- 我们正在为 .NET 4.5 使用 StackExchange.Redis 1.0.450。
死锁
当 application/service 启动时,它 运行 通常会持续一段时间,然后突然(几乎)所有传入的请求都停止运行,它们永远不会产生响应。所有这些请求都陷入僵局,等待对 Redis 的调用完成。
有趣的是,一旦发生死锁,对 Redis 的任何调用都会挂起,但前提是这些调用是从传入的 API 请求发出的,这些请求在线程池上 运行。
我们还从低优先级后台线程调用 Redis,这些调用即使在死锁发生后仍继续运行。
好像只有在线程池线程上调用Redis才会出现死锁。我不再认为这是由于这些调用是在线程池线程上进行的。相反,似乎任何异步 Redis 调用 没有继续,或者 sync safe 继续, 即使在发生死锁情况后也会继续工作. (请参阅下面的我认为会发生什么)
相关
StackExchange.Redis Deadlocking
混合
await
和Task.Result
造成的死锁(像我们一样同步异步)。但是我们的代码 运行 没有同步上下文,所以这里不适用,对吧?How to safely mix sync and async code?
是的,我们不应该那样做。但是我们这样做了,而且我们将不得不继续这样做一段时间。大量代码需要迁移到异步世界中。
同样,我们没有同步上下文,所以这不应该导致死锁,对吗?
在任何
await
之前设置ConfigureAwait(false)
对此没有影响。Timeout exception after async commands and Task.WhenAny awaits in StackExchange.Redis
这是线程劫持问题。目前这方面的情况如何?这可能是这里的问题吗?
StackExchange.Redis async call hangs
来自马克的回答:
...mixing Wait and await is not a good idea. In addition to deadlocks, this is "sync over async" - an anti-pattern.
但他也说:
SE.Redis bypasses sync-context internally (normal for library code), so it shouldn't have the deadlock
因此,根据我的理解,StackExchange.Redis 应该不知道我们是否在使用 sync-over-async 反模式。只是不推荐,因为它可能是 other 代码中死锁的原因。
然而,在这种情况下,据我所知,死锁确实在 StackExchange.Redis 内部。如有错误请指正
调试结果
我发现死锁似乎源于 line 124 of CompletionManager.cs
上的 ProcessAsyncCompletionQueue
。
该代码片段:
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);
}
我发现在死锁期间; activeAsyncWorkerThread
是我们正在等待 Redis 调用完成的线程之一。 (我们的线程 = 一个线程池线程运行宁我们的代码)。所以上面的循环被认为永远持续下去。
在不知道细节的情况下,这肯定感觉不对; StackExchange.Redis 正在等待它认为是 活动异步工作线程 的线程,而实际上它是一个与此完全相反的线程。
请问是不是线程劫持问题(我不是很懂)?
怎么办?
我想弄清楚的主要两个问题:
混合使用
await
和Wait()
/Result
是否会导致死锁,即使 运行ning 没有同步上下文?我们 运行 正在进入 StackExchange.Redis 中的 bug/limitation 吗?
可能的解决方法?
从我的调试结果来看,问题似乎在于:
next.TryComplete(true);
...on line 162 in CompletionManager.cs
在某些情况下可能会让当前线程(即 活动异步工作线程 )离开并开始处理其他代码,可能导致死锁。
不知道细节,只是考虑这个 "fact",那么在 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
}
}
我想我最大的希望是 Marc Gravell 会阅读这篇文章并提供一些反馈:-)
无同步上下文 = 默认同步上下文
我在上面写过我们的代码不使用 synchronization context. This is only partially true: The code is run as either a Console application or as an Azure Worker Role. In these environments SynchronizationContext.Current
是 null
,这就是为什么我写我们 运行ning without 同步上下文。
然而,在阅读 It's All About the SynchronizationContext 之后,我了解到事实并非如此:
By convention, if a thread’s current SynchronizationContext is null, then it implicitly has a default SynchronizationContext.
默认同步上下文不应该是死锁的原因,因为基于 UI 的(WinForms、WPF)同步上下文可能是 - 因为它并不暗示线程关联。
我认为会发生什么
消息完成后,将检查其完成源是否被视为同步安全。如果是,则内联执行完成操作,一切正常。
如果不是,想法是在新分配的线程池线程上执行完成操作。当 ConnectionMultiplexer.PreserveAsyncOrder
是 false
.
但是,当 ConnectionMultiplexer.PreserveAsyncOrder
为 true
(默认值)时,这些线程池线程将使用 完成队列 序列化它们的工作,并通过确保其中至多一个是 active async worker thread 在任何时候。
当一个线程成为活动的异步工作线程时,它将继续保持这种状态,直到它耗尽完成队列。
问题是完成操作不同步安全(从上面看),它仍然在不能被阻塞的线程上执行 因为这会阻止其他 非同步安全 消息完成。
请注意,其他通过 同步安全 完成操作完成的消息将继续正常工作,即使 活动异步工作线程 被屏蔽了。
我建议的"fix"(上文)不会以这种方式导致死锁,但是它会混淆保留异步完成顺序的概念。
所以这里可能要得出的结论是 当 PreserveAsyncOrder
是 true
,不管我们是不是运行没有同步上下文?
(至少在我们可以使用 .NET 4.6 和新的 TaskCreationOptions.RunContinuationsAsynchronously
之前,我想 )
我根据上面的详细信息进行了很多猜测,并且不知道您拥有的源代码。听起来您可能在 .Net 中遇到了一些内部的、可配置的限制。你不应该碰到那些,所以我的猜测是你没有处理对象,因为它们在线程之间浮动,这不允许你使用 using 语句来干净地处理它们的对象生命周期。
这详细说明了 HTTP 请求的限制。类似于旧的 WCF 问题,当您不处理连接时,所有 WCF 连接都会失败。
这更像是一种调试帮助,因为我怀疑您是否真的使用了所有 TCP 端口,但是关于如何找到您拥有多少个打开的端口以及到哪里的很好的信息。
https://msdn.microsoft.com/en-us/library/aa560610(v=bts.20).aspx
这些是我发现的解决此死锁问题的解决方法:
解决方法 #1
默认情况下 StackExchange.Redis 将确保命令的完成顺序与结果消息的接收顺序相同。如本问题中所述,这可能会导致死锁。
通过将 PreserveAsyncOrder
设置为 false
来禁用该行为。
ConnectionMultiplexer connection = ...;
connection.PreserveAsyncOrder = false;
这将避免死锁,也可以 improve performance。
我鼓励 运行 遇到死锁问题的任何人尝试此解决方法,因为它非常干净和简单。
您将无法保证异步延续的调用顺序与底层 Redis 操作的完成顺序相同。但是,我真的不明白为什么你会依赖它。
解决方法 #2
当 StackExchange.Redis 中的 活动异步工作线程 完成命令并且内联执行完成任务时发生死锁。
可以通过使用自定义 TaskScheduler
and ensure that TryExecuteTaskInline
returns 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...
}
实施一个好的任务调度程序可能是一项复杂的任务。但是,ParallelExtensionExtras library (NuGet package) 中的现有实现可供您使用或从中汲取灵感。
如果您的任务调度程序将使用自己的线程(而不是来自线程池),那么允许内联可能是个好主意,除非当前线程来自线程池。这将起作用,因为 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);
}
另一个想法是使用 thread-local storage.
将调度程序附加到它的所有线程private static ThreadLocal<TaskScheduler> __attachedScheduler
= new ThreadLocal<TaskScheduler>();
确保在线程启动时分配此字段 运行ning 并在其完成时清除:
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;
}
}
然后您可以允许内联任务,只要它由自定义调度程序在 "owned" 的线程上完成即可:
public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
// Allow inlining on our own threads.
return __attachedScheduler.Value == this && this.TryExecuteTask(task);
}