联锁类:先读后写争用条件问题

Interlocked Class: read before write race condition problem

提问人:user22422035 提问时间:8/21/2023 最后编辑:providerZuser22422035 更新时间:8/22/2023 访问量:52

问:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace InterlockedLearning
{
    class Program
    {
        static int sharedVariable = 0;

        static void Main()
        {
            Parallel.For(0, 1000000, Func1);
            Console.WriteLine("Thread{0} sharedVariable: {1}", Thread.CurrentThread.ManagedThreadId, sharedVariable);
            Console.Read();
        }

        public static void Func1(int val)
        {
            int i = Interlocked.CompareExchange(ref sharedVariable, 0, 0);
            if (i < 500000)
            {
                Interlocked.Increment(ref sharedVariable);
            }
        }
}

我正在研究如何修改上述代码,以便不再发生争用条件问题。

上面的代码结果应该是 500000,但如果运行多次,代码的最终结果500001。我认为这可能是由检查条件引起的。

我知道它可以通过简单地使用锁来解决,但我想知道是否有任何非阻塞风格的方法可以解决这个问题。

C# 多线程 并发 非阻塞 互锁

评论


答:

2赞 Guru Stron 8/21/2023 #1

我认为这可能是由检查条件引起的。

是的,没错,读取然后检查它不是原子操作 - 想象一下 2 个线程(A 和 B)做(为什么不只是)获取499999,执行检查然后递增值。例如:int iint i = Interlocked.CompareExchange(ref sharedVariable, 0, 0);Interlocked.Read

  • 线程 A 读取 sharedVariable = 499999
  • 线程 A 检查 sharedVariable < 500000 (true)
  • 线程 B 读取 sharedVariable = 499999
  • 线程 B 检查 sharedVariable < 500000 (true)
  • 线程 A 递增 sharedVariable (500000)
  • 线程 B 增量 sharedVariable (500001)

我知道它可以通过简单地使用锁来解决,但我想知道是否有任何非阻塞样式的方法来解决这个问题。

我能想到的不多,但例如,您可以在溢出的情况下减少:

public static void Func1(int val)
{
    if (sharedVariable >= 500000) return;
    if (Interlocked.Increment(ref sharedVariable) > 500000)
    {
        Interlocked.Decrement(ref sharedVariable);
    }
}

另一种选择是循环使用(请参阅文档中的示例)。CompareExchange

附言

强烈建议您查看 Deadlock Empire,这将使理解此类情况变得更加容易。

评论

0赞 user22422035 8/21/2023
public static void Func1(int val) { if (Interlocked.Read(ref sharedVariable) < 500000) { Interlocked.Increment(ref sharedVariable);我改用 Interlocked.Read() 修改了代码,竞争条件问题再次发生
2赞 Guru Stron 8/21/2023
@user22422035 是的,因为它有所有相同的问题 - 其次是非原子的比较(2 个单独的操作)。这句话是关于你的用法,没有多大意义,因为它实际上只是你正在寻找的。ReadCompareExchangeRead
3赞 canton7 8/21/2023 #2

使用以下方法更新字段的一般模式是(来自 MSDN):CompareExchange

int initialValue, computedValue;
do {
    // Save the current running total in a local variable.
    initialValue = totalValue;

    // Add the new value to the running total.
    computedValue = initialValue + addend;

    // CompareExchange compares totalValue to initialValue. If
    // they are not equal, then another thread has updated the
    // running total since this loop started. CompareExchange
    // does not update totalValue. CompareExchange returns the
    // contents of totalValue, which do not equal initialValue,
    // so the loop executes again.
} while (initialValue != Interlocked.CompareExchange(ref totalValue, computedValue, initialValue));

根据你的方案进行调整:Adapting this to your scenario:

int initialValue, computedValue;
do
{
    initialValue = sharedVariable;
    if (initialValue >= 500000)
    {
        break;
    }
    computedValue = initialValue + 1;
} while (initialValue != Interlocked.CompareExchange(ref sharedVariable, computedValue, initialValue));

这说:

  • 读取字段的当前值。
  • 如果当前值为 >= 500000,则完成。
  • 通过添加 1 来计算新值
  • 如果该字段自我们读取以来未发生更改,请将其更新为新的计算值。如果自从我们读到它以来它发生了变化,那么有一场比赛,我们需要再试一次。

有可能有一场比赛,我们读取 ,然后其他人递增它,然后我们与 500000 进行比较,但这没关系。如果在其他人递增之前它是 >= 500000,那么在他们递增它之后,它也将是 >= 500000。sharedVariable

评论

1赞 Theodor Zoulias 8/21/2023
虽然这在技术上是正确的解决方案,但它的性能可能比简单的 .问题的作者不应该认为无锁等于更快。lock
2赞 canton7 8/21/2023
绝对。在寻找性能时始终以基准为基准