【发布时间】:2017-01-23 17:18:07
【问题描述】:
我有一个每 5 秒检查一次工作的 Windows 服务。它使用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 个 Timer 线程进入 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 秒,并在 Timer 触发另一个回调时正确执行,他们都以某种方式进入。这就是我选择 Monitor.TryEnter 选项的原因,所以我不会只是继续堆叠计时器线程。
有什么线索吗?在我之前尝试解决此问题的所有情况下,System.Threading.Timer 一直是不变的,我认为这是根本原因,但我不明白为什么。
【问题讨论】:
-
只是好奇,
Details.StillGettingWork(或其支持字段)是否标记为volatile? -
@itsme86
Details是一个实例类,StillGettingWork是一个自动属性。没有任何东西被标记为 volatile。 -
这不就是为什么要创建互斥锁的原因吗? msdn.microsoft.com/en-us/library/windows/hardware/…
-
@fernando.reyes 互斥锁不会导致所有其他工作线程无法检查自己的工作吗?我认为这就是创建监视器的原因。这是一名工人试图检查自己的工作,我需要确保 它 只检查一次工作。
-
您将问题描述为:“我有 2 个线程独立尝试处理导致工作失败”,因此您认为单个工作实例同时在 2 个线程中运行该方法导致失败?看来您的架构是“多个工作人员,每个工作人员使用多个线程”?对于单个工作人员,您不能将 2 个线程“以某种方式使其”进入锁定部分,并且您的锁看起来很好。您的
CheckForWork();更有可能将相同的工作发送给不同的工作人员,而不是单个工作人员在同一个锁定部分中有 2 个线程。
标签: c# .net multithreading windows-services