【问题标题】:Monitor.TryEnter and Threading.Timer race conditionMonitor.TryEnter 和 Threading.Timer 竞争条件
【发布时间】: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


【解决方案1】:

TL;DR
生产存储过程多年未更新。工作人员正在获得他们本不应该得到的工作,因此多个工作人员正在处理更新请求。


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

那些假设多个工人正在接手工作的假设确实是正确的,这是永远不可能发生的事情,因为每个工人在他们所做和要求的工作中都是独一无二的。

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

谢天谢地,修复是数据库方面的,而不是客户端更新。

【讨论】:

    【解决方案2】:

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

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

    static object _workCheckLocker;
    

    并通过初始化该字段为您的类引入静态构造函数(如果是内联初始化,您可能会面临一些更复杂的问题),但我不确定这是否足以满足您的情况 - 在AppDomain重新启动静态类也会重新加载。据我了解,这不是您的选择。

    也许您可以为您的工作人员引入static 字典而不是对象,这样您就可以检查Id 以获取正在处理的文档。

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

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

    【讨论】:

    • AppDomain 用于加载执行工作人员需要的实际处理的类。工人是通用的。当它获得更新时,它基本上会自我更新。非常感谢您的时间。我只需要找到一天可以锤击它并尝试通过 Visual Studio 重现它。
    • 哦,好的。我认为您提供的代码是线程安全的。也许,出于某种原因,两个不同的工作人员要处理同一个文件。
    猜你喜欢
    • 2020-07-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-23
    • 2018-10-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多