【问题标题】:Are locks sequentially consistent?锁是顺序一致的吗?
【发布时间】:2018-10-31 09:18:00
【问题描述】:

这是这个问题的 C# 版本:Is std::mutex sequentially consistent?

简而言之:如果多个线程对不同的对象进行锁定,它们是否保证在这些不同的锁定中看到相同的事件顺序?

这里是演示:

internal sealed class Test
{
    private readonly object lockerA = new object();
    private bool valueA;

    private readonly object lockerB = new object();
    private bool valueB;

    public void RunTest()
    {
        var taskA = Task.Run(() =>
        {
            lock (lockerA)
                valueA = true;
        });
        var taskB = Task.Run(() =>
        {
            lock (lockerB)
                valueB = true;
        });
        var taskC = Task.Run(() =>
        {
            // Reads A, then B.
            bool readA;
            lock (lockerA)
                readA = valueA;

            bool readB;
            lock (lockerB)
                readB = valueB;

            return (readA, readB);
        });
        var taskD = Task.Run(() =>
        {
            // Reads B, then A.
            bool readB;
            lock (lockerB)
                readB = valueB;

            bool readA;
            lock (lockerA)
                readA = valueA;

            return (readA, readB);
        });

        Task.WaitAll(taskA, taskB, taskC, taskD);

        if (taskC.Result == (readA:true, readB:false) && taskD.Result == (readA:false, readB:true))
        {
            // Can this happen?
            Console.WriteLine("Ordering inconsistency!");
        }
    }
}

编辑:

修复了Matthew Watson 显示示例即使具有顺序一致性也失败后的错误。

【问题讨论】:

  • 需要注意的一点是bool 的赋值是原子操作,由于您只是锁定bool 的赋值,因此锁定实际上没有任何好处。完全没有任何锁,代码也能正常工作。
  • 我的意思是锁对你想要的原子操作没有帮助,也就是说,设置 A 和 B 并读取 A 和 B。要做到这一点,您必须锁定读取和写入这两个变量的代码。
  • 我在您的other question 上发布的涵盖执行顺序的 C# 规范部分就是关于 C# 重新排序的讨论。在引用的部分省略中,它还讨论了lock
  • 即使特定平台上的当前行为碰巧是“连续的”,我可以告诉你,规范中并不能保证它,因此 a) 取决于实施和 b) 可能会发生变化。我显然知道代码不实用,如果您依赖于避免您描述为“不一致”的行为,那么将其扩展到任何实用代码都是一种糟糕的设计模式。例如,它在 ARM、Mono、.NET vNext 或 Intel 芯片 vNext 上的行为可能有所不同。
  • @MikeMarynowski “即使特定平台上的当前行为恰好是“连续的”,我可以告诉你,规范中并不能保证这一点”这就是被问到的问题。所有其他 cmets 都是关于我应该或不应该编写什么样的代码的不必要的和题外话的讨论。

标签: c# multithreading thread-safety language-lawyer


【解决方案1】:

锁不保证顺序一致性。相反,它保证如果一个线程在需要一组资源的情况下启动一个操作,那么在具有锁的线程完成对资源的使用之前,其他线程都不能访问(和更改)这些资源。哪个线程首先尝试访问资源取决于一大堆锁并不真正关心的其他因素。

在您希望确保对象在该操作期间不被其他线程修改的操作中使用锁。哪个操作首先访问线程 - 那是完全不同的故事。

请参阅lock statement documentation

【讨论】:

  • 认为锁定一个锁会阻止其他线程使用某些资源是一个常见的新手错误。 (这在任何Object 可以用作锁的语言中尤其常见。)但这不是锁所阻止的。它们只防止多个线程同时锁定同一个锁。保护资源是开发人员的责任(即确保他/她的代码在持有适当的锁时不会访问资源。)
  • @SolomonSlow 我同意。只是普通用途成为最简单的解释。通过防止锁定同一个对象,您可以使用锁定块保护正在访问的资源。
  • 当然,简单就是好,“常用”将加快您与有经验的程序员的对话。但是许多寻求并找到这个答案的人将是新程序员,他们不会理解常见用法被过度简化(即,实际上不是真的)
【解决方案2】:

要查看理论上这是否可以返回 (false, true) 和 (true, false),我们只需要展示一组交错的步骤,任务理论上可以执行该结果。

下面是这样一组步骤:

Init: A = false, B = false

Task A: starting up
Task B: starting up
Task C: enter lock A
Task C: read A = false   C.A = false
Task C: leave lock A
Task A: enter lock A
Task A: set A = true     A = true
Task A: leave lock A
Task A: exit
Task D: enter lock B
Task D: read B = false   D.B = false
Task D: leave lock B
Task B: enter lock B
Task B: set B = true     B = true
Task B: leave lock B
Task B: exit
Task C: enter lock B
Task C: read B = true    C.B = true
Task C: leave lock B
Task C: exit returning (false, true)
Task D: enter lock A
Task D: read A = true    D.A = true
Task D: leave lock A
Task D: exit returning (true, false)

Now task C returned (false, true) and task D returned (true, false)

这证明理论上可以返回乱序结果。

【讨论】:

  • 并且语言规范不保证这种理论上的情况不会发生,所以最好不要指望它,即使它恰好在某个平台上使用某个运行时以某种未记录的方式表现现在。如果不一致永远不会发生,那么问题中的“模式”很容易避免,而不会牺牲任何性能,如果发生了,那么无论如何它都是不可用的。
  • 您证明了我的示例是一个糟糕的示例,因为它可以在完全一致的读取和写入序列中报告“不一致”。我更新了问题。
猜你喜欢
  • 2023-03-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-31
  • 2011-09-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多