Monitor.TryEnter和Threading.Timer竞争条件

时间:2016-09-15 15:40:54

标签: c# .net multithreading windows-services

我有一个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个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选项所以我不会#39; t只是保持堆叠计时器线程。

有任何线索吗?在我之前试图解决这个问题的所有情况下,System.Threading.Timer一直是常数,我认为它是根本原因,但我不明白为什么。

2 个答案:

答案 0 :(得分:0)

我可以在日志中看到你提供了那里AppDomain重启,这是正确的吗?如果是,您确定在AppDomain重启期间,您的服务只有一个是唯一的一个对象吗?我认为在此期间并非所有线程都在同一时间停止,并且其中一些线程可以继续轮询工作队列,因此不同AppDomain中的两个不同线程具有相同的{{1}工作。

您可以通过使用Id关键字标记_workCheckLocker来解决此问题,如下所示:

static

并为您的类引入静态构造函数,并初始化此字段(如果是内联初始化,您可能会遇到一些更复杂的问题),但我不确定这是否适合您的情况 - 在{{1重启静态类也会重新加载。据我了解,这不是你的选择。

也许您可以为您的员工引入static object _workCheckLocker; 字典而不是对象,因此您可以查看AppDomain正在处理的文档。

另一种方法是处理您的服务的static事件,这可能会在Id重启期间调用,您将在其中引入CancellationToken,并使用它来停止在这种情况下的所有工作。

另外,正如@ fernando.reyes所说,你可以为同步引入称为互斥锁的重锁结构,但这会降低你的性能。

答案 1 :(得分:0)

<强> TL; DR
生产存储过程多年未更新。工人们正在从事他们应该从未得过的工作,因此多名工人正在处理更新请求。

我终于找到时间在本地正确设置自己,通过Visual Studio充当生产客户端。虽然,我无法像我经历的那样重现它,但我偶然发现了这个问题。

那些假设多个工人正在接受工作的人确实是正确的,并且由于每个工人在他们所做和要求的工作中都是独一无二的,所以这是不可能实现的。

事实证明,在我们的生产环境中,基于工作类型检索工作的存储过程尚未在部署年份(是,年!)中更新。任何检查工作的东西都会自动获得更新,这意味着当Update工作人员和工人Foo同时检查时,他们最终都得到了相同的工作。

值得庆幸的是,修复程序是数据库端而不是客户端更新。