Monitor.TryEnter 和 Threading.Timer 竞争条件
Monitor.TryEnter and Threading.Timer race condition
我有一个 Windows 服务,每 5 秒检查一次工作。它使用 System.Threading.Timer
来处理检查和处理,并使用 Monitor.TryEnter
来确保只有一个线程正在检查工作。
假设它必须是这种方式,因为以下代码是服务创建的其他 8 个工作人员的一部分,每个工作人员都有自己需要检查的特定工作类型。
readonly object _workCheckLocker = new object();
public Timer PollingTimer { get; private set; }
void InitializeTimer()
{
if (PollingTimer == null)
PollingTimer = new Timer(PollingTimerCallback, null, 0, 5000);
else
PollingTimer.Change(0, 5000);
Details.TimerIsRunning = true;
}
void PollingTimerCallback(object state)
{
if (!Details.StillGettingWork)
{
if (Monitor.TryEnter(_workCheckLocker, 500))
{
try
{
CheckForWork();
}
catch (Exception ex)
{
Log.Error(EnvironmentName + " -- CheckForWork failed. " + ex);
}
finally
{
Monitor.Exit(_workCheckLocker);
Details.StillGettingWork = false;
}
}
}
else
{
Log.Standard("Continuing to get work.");
}
}
void CheckForWork()
{
Details.StillGettingWork = true;
//Hit web server to grab work.
//Log Processing
//Process Work
}
问题来了:
上面的代码允许 2 个定时器线程进入 CheckForWork()
方法。老实说,我不明白这是怎么可能的,但我在这个软件 运行.
的多个客户中都遇到过这种情况
我今天推送一些工作时得到的日志显示它检查了两次工作,我有 2 个线程独立尝试处理,这一直导致工作失败。
Processing 0-3978DF84-EB3E-47F4-8E78-E41E3BD0880E.xml for Update Request. - at 09/14 10:15:501255801
Stopping environments for Update request - at 09/14 10:15:501255801
Processing 0-3978DF84-EB3E-47F4-8E78-E41E3BD0880E.xml for Update Request. - at 09/14 10:15:501255801
Unloaded AppDomain - at 09/14 10:15:10:15:501255801
Stopping environments for Update request - at 09/14 10:15:501255801
AppDomain is already unloaded - at 09/14 10:15:501255801
=== Starting Update Process === - at 09/14 10:15:513756009
Downloading File X - at 09/14 10:15:525631183
Downloading File Y - at 09/14 10:15:525631183
=== Starting Update Process === - at 09/14 10:15:525787359
Downloading File X - at 09/14 10:15:525787359
Downloading File Y - at 09/14 10:15:525787359
日志是异步写入并排队的,所以不要深挖时间完全匹配的事实,我只是想指出我在日志中看到的内容以表明我有 2 个线程命中了一段我认为永远不应该被允许的代码。 (虽然日志和时间是真实的,只是经过清理的消息)
最终发生的事情是 2 个线程开始下载足够大的文件,其中一个线程最终拒绝访问该文件并导致整个更新失败。
上面的代码怎么会允许这样做呢?去年我遇到了这个问题,当时我有一个 lock
而不是 Monitor
并假设这只是因为计时器最终开始得到足够的偏移量,因为我是 lock
阻塞让计时器线程堆叠,即一个阻塞 5 秒,并在计时器触发另一个回调时通过,并且它们都以某种方式进入。这就是为什么我使用 Monitor.TryEnter
选项,所以我不会只是继续堆叠计时器线程。
有线索吗?在我之前尝试解决此问题的所有情况下,System.Threading.Timer
一直是一个常数,我认为这是根本原因,但我不明白为什么。
我可以在日志中看到您提供了 AppDomain
重启,对吗?如果是,您确定 和 在 AppDomain
重启期间您的服务只有一个对象吗?我认为在此期间并非所有线程都同时停止,并且其中一些线程可以继续轮询工作队列,因此不同 AppDomain
s 中的两个不同线程得到相同的 Id
为了工作。
您可能可以通过使用 static
关键字标记您的 _workCheckLocker
来解决此问题,如下所示:
static object _workCheckLocker;
并为您的 class 引入静态构造函数并初始化此字段(如果是内联初始化,您可能会遇到一些更复杂的问题),但我不确定这是否足以满足您的需求案例 - 在 AppDomain
重启期间静态 class 也会重新加载。据我了解,这不适合您。
也许您可以为您的工作人员引入 static
字典而不是对象,这样您就可以检查 Id
中的文档。
另一种方法是为您的服务处理 Stopping
事件,该事件可能会在 AppDomain
重启期间调用,其中您将引入 CancellationToken
并使用它在这种情况下停止所有工作。
此外,正如@fernando.reyes所说,您可以引入称为互斥量的重锁结构来进行同步,但这会降低您的性能。
TL;DR
生产存储过程多年未更新。工作人员得到了他们本不应该得到的工作,因此多个工作人员正在处理更新请求。
通过 Visual Studio,我终于找到时间在本地正确设置自己以充当生产客户端。虽然,我无法像我所经历的那样重现它,但我确实偶然发现了这个问题。
那些假设多个工人正在接手工作的人确实是正确的,这是永远不可能发生的事情,因为每个工人在他们所做和要求的工作中都是独一无二的。
事实证明,在我们的生产环境中,根据工作类型检索工作的存储过程在多年(是的,多年!)的部署中没有更新。任何检查工作的东西都会自动获得更新,这意味着当 Update worker 和 worker Foo 同时检查时,它们最终都会完成相同的工作。
谢天谢地,修复是数据库端的,而不是客户端更新。
我有一个 Windows 服务,每 5 秒检查一次工作。它使用 System.Threading.Timer
来处理检查和处理,并使用 Monitor.TryEnter
来确保只有一个线程正在检查工作。
假设它必须是这种方式,因为以下代码是服务创建的其他 8 个工作人员的一部分,每个工作人员都有自己需要检查的特定工作类型。
readonly object _workCheckLocker = new object();
public Timer PollingTimer { get; private set; }
void InitializeTimer()
{
if (PollingTimer == null)
PollingTimer = new Timer(PollingTimerCallback, null, 0, 5000);
else
PollingTimer.Change(0, 5000);
Details.TimerIsRunning = true;
}
void PollingTimerCallback(object state)
{
if (!Details.StillGettingWork)
{
if (Monitor.TryEnter(_workCheckLocker, 500))
{
try
{
CheckForWork();
}
catch (Exception ex)
{
Log.Error(EnvironmentName + " -- CheckForWork failed. " + ex);
}
finally
{
Monitor.Exit(_workCheckLocker);
Details.StillGettingWork = false;
}
}
}
else
{
Log.Standard("Continuing to get work.");
}
}
void CheckForWork()
{
Details.StillGettingWork = true;
//Hit web server to grab work.
//Log Processing
//Process Work
}
问题来了:
上面的代码允许 2 个定时器线程进入 CheckForWork()
方法。老实说,我不明白这是怎么可能的,但我在这个软件 运行.
我今天推送一些工作时得到的日志显示它检查了两次工作,我有 2 个线程独立尝试处理,这一直导致工作失败。
Processing 0-3978DF84-EB3E-47F4-8E78-E41E3BD0880E.xml for Update Request. - at 09/14 10:15:501255801
Stopping environments for Update request - at 09/14 10:15:501255801
Processing 0-3978DF84-EB3E-47F4-8E78-E41E3BD0880E.xml for Update Request. - at 09/14 10:15:501255801
Unloaded AppDomain - at 09/14 10:15:10:15:501255801
Stopping environments for Update request - at 09/14 10:15:501255801
AppDomain is already unloaded - at 09/14 10:15:501255801
=== Starting Update Process === - at 09/14 10:15:513756009
Downloading File X - at 09/14 10:15:525631183
Downloading File Y - at 09/14 10:15:525631183
=== Starting Update Process === - at 09/14 10:15:525787359
Downloading File X - at 09/14 10:15:525787359
Downloading File Y - at 09/14 10:15:525787359
日志是异步写入并排队的,所以不要深挖时间完全匹配的事实,我只是想指出我在日志中看到的内容以表明我有 2 个线程命中了一段我认为永远不应该被允许的代码。 (虽然日志和时间是真实的,只是经过清理的消息)
最终发生的事情是 2 个线程开始下载足够大的文件,其中一个线程最终拒绝访问该文件并导致整个更新失败。
上面的代码怎么会允许这样做呢?去年我遇到了这个问题,当时我有一个 lock
而不是 Monitor
并假设这只是因为计时器最终开始得到足够的偏移量,因为我是 lock
阻塞让计时器线程堆叠,即一个阻塞 5 秒,并在计时器触发另一个回调时通过,并且它们都以某种方式进入。这就是为什么我使用 Monitor.TryEnter
选项,所以我不会只是继续堆叠计时器线程。
有线索吗?在我之前尝试解决此问题的所有情况下,System.Threading.Timer
一直是一个常数,我认为这是根本原因,但我不明白为什么。
我可以在日志中看到您提供了 AppDomain
重启,对吗?如果是,您确定 和 在 AppDomain
重启期间您的服务只有一个对象吗?我认为在此期间并非所有线程都同时停止,并且其中一些线程可以继续轮询工作队列,因此不同 AppDomain
s 中的两个不同线程得到相同的 Id
为了工作。
您可能可以通过使用 static
关键字标记您的 _workCheckLocker
来解决此问题,如下所示:
static object _workCheckLocker;
并为您的 class 引入静态构造函数并初始化此字段(如果是内联初始化,您可能会遇到一些更复杂的问题),但我不确定这是否足以满足您的需求案例 - 在 AppDomain
重启期间静态 class 也会重新加载。据我了解,这不适合您。
也许您可以为您的工作人员引入 static
字典而不是对象,这样您就可以检查 Id
中的文档。
另一种方法是为您的服务处理 Stopping
事件,该事件可能会在 AppDomain
重启期间调用,其中您将引入 CancellationToken
并使用它在这种情况下停止所有工作。
此外,正如@fernando.reyes所说,您可以引入称为互斥量的重锁结构来进行同步,但这会降低您的性能。
TL;DR
生产存储过程多年未更新。工作人员得到了他们本不应该得到的工作,因此多个工作人员正在处理更新请求。
通过 Visual Studio,我终于找到时间在本地正确设置自己以充当生产客户端。虽然,我无法像我所经历的那样重现它,但我确实偶然发现了这个问题。
那些假设多个工人正在接手工作的人确实是正确的,这是永远不可能发生的事情,因为每个工人在他们所做和要求的工作中都是独一无二的。
事实证明,在我们的生产环境中,根据工作类型检索工作的存储过程在多年(是的,多年!)的部署中没有更新。任何检查工作的东西都会自动获得更新,这意味着当 Update worker 和 worker Foo 同时检查时,它们最终都会完成相同的工作。
谢天谢地,修复是数据库端的,而不是客户端更新。