【问题标题】:Lock Isn't Stopping Duplicates [closed]锁定不会停止重复[关闭]
【发布时间】:2015-12-18 17:09:39
【问题描述】:

我的代码的 Lock() 关键部分有问题。情况是我的输出包含重复项,我不能这样!您可以在下面看到复制我的情况的代码。我的情况真的很愚蠢,如果我错过了一些非常愚蠢的东西,请原谅......

private static object theLock = new object();
private static int currentNumber = 0;

。 . .

static void Main(string[] args)
{
    for (int i = 0; i < 30; i++)
        CreateAndRunWorker();
}

。 . .

private static void CreateAndRunWorker()
{
    BackgroundWorker worker = new BackgroundWorker();
    worker.DoWork += new DoWorkEventHandler(TheWorkToBeDone);
    worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(TheWorkAfterWork);
    worker.RunWorkerAsync();
}

。 . .

private static void TheWorkToBeDone(object sender, DoWorkEventArgs e)
{
    OutputNum();
}

。 . .

private static void TheWorkAfterWork(object sender, RunWorkerCompletedEventArgs e)
{
     CreateAndRunWorker();
}

。 . .

private static void OutputNum()
{
    lock (theLock)
    {
        currentNumber++;
        Console.WriteLine(currentNumber);
    }
}

上面的内容被 25 个后台工作人员从 Main() 函数调用。锁和计数器是全局初始化的。输出包含重复的数字。如何?

【问题讨论】:

  • 向我们展示更多代码。是在winforms吗?您如何以及在何处致电OutputNum
  • 我会说您可能正在某处重新设置“theLock”。无论如何,您向我们展示的代码不包含问题。
  • 我将添加更多代码,一秒钟。它是一个控制台应用程序。
  • @bokibeg 好吧,代码最终会溢出int 并再次开始重复值。由于提供的信息不完整,尚不清楚他是否观察到了这一点。
  • 这个问题的主要内容是给出的代码工作正常

标签: c# .net multithreading locking


【解决方案1】:

您可能会在不相关的内核上遇到一些本地缓存的缓存效果。手头的问题是int 本身没有以确保本地缓存保持最新的方式声明或更新。您可以通过以下几种方式解决此问题:

修复您的实施

private static readonly object theLock = new object();
private static volatile int currentNumber = 0; // note the addition of "volatile"

private static void OutputNum()
{
    lock (theLock)
    {
        currentNumber++;
        Console.WriteLine(currentNumber);
    }
}

volatile 关键字对于强制所有本地缓存​​的对该数字的引用读取相同非常重要。在微优化中存在一些极端情况,其中无序执行可能会在某些芯片架构上导致竞争条件。不过,仍然需要锁来防止增量竞争条件。

readonly 关键字对于防止您的锁定对象在运行时被覆盖很重要。一旦初始化,readonly 锁定对象的实例就永远无法更改。如果您确实更改了锁对象的实例,您将有很短的时间让两个线程可以同时访问计数器变量。

解开锁

如果你使用 Interlocked.Increment 函数,你完全不用锁就可以得到你想要的效果。

private static volatile int currentNumber = 0;

private static void OutputNum()
{
    int localRef = Interlocked.Increment(ref currentNumber);
    Console.WriteLine(localRef);
}

这完全消除了锁,使解决方案的性能更高,因为锁争用较少,但保留了所有的安全性。

重要提示: 使用从 Interlocked.Increment 返回的值以供将来在同一方法中使用,因为这不会因另一个线程访问相同的代码并在 Console.WriteLine() 可以之前更改值而改变执行。

了解为什么会这样

  1. CLR 在运行时执行优化并重新编译代码。 volatile 关键字确保优化不包括访问您的计数器。 (https://msdn.microsoft.com/en-us/library/x13ttww7.aspx)
  2. 如果锁实例在运行时发生变化,您将有一个短暂的时间出现竞争条件。一个线程锁定在旧对象上,而另一个线程锁定在新对象上。始终声明您的锁定对象,以便它们无法更改。如果它是一个静态对象,那么 static readonlyconst 就是你想要的。如果它是用于保护一个实例的对象内的字段,则将锁声明为readonlyreadonly 关键字确保实例在包含范围的生命周期内永远不会改变。 (https://msdn.microsoft.com/en-us/library/acdd6hb7.aspx)
  3. Interlocked.Increment 执行原子更改,确保所有运行内核的安全。如果您的代码的关键部分只是增加一个计数器,则此方法可以减轻系统对重锁的需求。只需将返回值用于方法的其余部分,以便它在该范围内保持不变。 (https://msdn.microsoft.com/en-us/library/dd78zt0c(v=vs.110).aspx)

有关该主题的良好摘要,请参阅https://stackoverflow.com/a/154803/476048

出于学术目的,Java 中存在相同的问题和解决方案。 API 只是略有不同。

【讨论】:

  • lock 语句已经专门添加了内存屏障来防止这种情况。使变量 volatile 不会改变任何东西。移除锁只是降低代码的安全性,而不是提高它。
  • 感谢您的帮助。不幸的是,切换到 volatile 并不能解决问题。
  • @savvy,阅读我附在底部的文章。另外,您对多核 CPU 中的本地缓存如何工作有很好的了解吗?您的 cmets 清楚地表明,您对 C# 提供的机制的理解非常不完整。并且 Interlocked.Increment 是完全线程安全的。试试看。
  • @RobertSteinerIII 所以你根本不关心得到一个可行的解决方案,因为你一开始就没有真正的问题?那么,如果您不关心获得实际可行的解决方案,为什么还要问这个问题呢?当您不在乎您的解决方案是否有效时,为什么要鼓励某人给您一个无效的解决方案?为什么要试图让其他人使用一个不起作用的解决方案只是因为你不在乎你是否有一个有效的解决方案?你基本上是在说你是一个巨魔,因为你可以。
  • @Berin Loritsch 您的解释不正确。锁提供必要的障碍/栅栏。否则,语言将完全被破坏。
猜你喜欢
  • 2021-10-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多