1

This is a C# version of this question: Is std::mutex sequentially consistent?

In short: if multiple threads take locks on different objects, are they guaranteed to see the same order of events within those different locks?

Here is the demonstration:

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!");
        }
    }
}

EDIT:

Fixed an error after Matthew Watson showed that the example fails even with sequential consistency.

relatively_random
  • 4,505
  • 1
  • 26
  • 48
  • One thing to note is that assignment of `bool` is an atomic operation, and since you are only locking the assignment of `bool`, the locks actually have no benefit whatsoever. The code would work the same without any of the locks at all. – Matthew Watson Oct 31 '18 at 09:43
  • @MatthewWatson He's locking on the read as well, I'm not sure what you mean? – Mike Marynowski Oct 31 '18 at 09:47
  • @MikeMarynowski I mean that because the operation being locked (both the read AND the write) is atomic, then locking will have no effect, since there is no possibility of a torn read from a `bool`. – Matthew Watson Oct 31 '18 at 09:50
  • @MatthewWatson A lock will ensure a fresh read instead of a stale cache value and it will place guarantees on operations not being reordered. – Mike Marynowski Oct 31 '18 at 09:51
  • @MatthewWatson Without the locks, the ordering inconsistency certainly *can* happen. The question is, can it happen with locks in place? – relatively_random Oct 31 '18 at 10:01
  • @relatively_random To be perfectly frank I'm not sure at the moment, but I can't imagine depending on this behavior being a good idea, design wise or sanity wise. If you need a consistent snapshot of A and B then use nested locks that always come out in the same order. – Mike Marynowski Oct 31 '18 at 10:04
  • 1
    What I'm getting at is that the locks don't help with what you want the atomic operation to be, that is, setting BOTH A and B and reading BOTH A and B. To do that, you'd have to lock around the code that reads and writes both the variables. – Matthew Watson Oct 31 '18 at 10:05
  • I'm not asking whether this is a good idea, I just want to know what the guarantees of the lock are and what they aren't. The example provided is obviously not a piece of practical code. – relatively_random Oct 31 '18 at 10:09
  • 1
    The section of the C# spec that covers Execution Order, that I posted on your [other question](https://stackoverflow.com/a/53080547/15498) is just about it so far as the discussion of reordering goes for C#. In part of the elisions from the quote it also discussed `lock`. – Damien_The_Unbeliever Oct 31 '18 at 10:11
  • 1
    Even if current behavior on a particular platform happens to be "sequential", I can tell you that it's not guaranteed in the spec and thus a) up to the implementation and b) subject to change. I obviously know the code isn't practical, it's just a bad design pattern to extend to any practical code if you depend on avoiding the behavior you are describing as "inconsistent". It may behave differently on ARM for example, or Mono, or .NET vNext, or Intel chip vNext. – Mike Marynowski Oct 31 '18 at 10:30
  • 1
    @MikeMarynowski "Even if current behavior on a particular platform happens to be "sequential", I can tell you that it's not guaranteed in the spec" This is what was being asked. All the other comments are an unnecessary and off-topic discussion about what kind of code I should or shouldn't write. – relatively_random Oct 31 '18 at 11:45

2 Answers2

2

Locks do not guarantee sequential consistency. Rather, it guarantees that if a thread starts an operation where it requires a set of resources, no other thread can access (and alter) those resources till the thread with the lock is done with the resources. Which thread attempts to access the resources first is subject to a whole bunch of other factors which the lock doesn't really care about.

Use locks in operations where u want guarantee that an object(s) is not modified by other threads during that operations. Which operation accesses the thread first - thats a whole different story.

See the lock statement documentation.

Gerald Chifanzwa
  • 1,277
  • 7
  • 14
  • 2
    It's a common newbie mistake to think that locking a lock will prevent other threads from using some resource. (It's especially common in those languages where any `Object` can be used as a lock.) But that's not what locks prevent. They only prevent multiple threads from locking the same lock at the same time. Protecting the resources is the _developer's_ responsibility (i.e., to ensure that his/her code never accesses the resource(s) except when holding the appropriate lock.) – Solomon Slow Oct 31 '18 at 13:06
  • @SolomonSlow i agree. Just that the common use becomes the simplest explanation. By preventing locking the same object, u protect the resources being accessed withing the lock block. – Gerald Chifanzwa Oct 31 '18 at 15:07
  • Sure, simple is good, and "the common use" will speed up your conversations with experienced programmers. But many of the folks who seek and find this answer will be new programmers who will not understand that the common usage is _over_ simplified (i.e., not actually true) – Solomon Slow Oct 31 '18 at 17:24
1

To see if this could theoretically return (false, true) and (true, false) we just need to show a set of interleaved steps the tasks could theoretically perform with that result.

Here is such a set of steps:

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)

That proves that theoretically an out of sequence result could be returned.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • And the language spec makes no guarantees that this theoretical situation will not happen, so best not to bank on it even if it just so happens to behave a certain undocumented way on a certain platform using a certain runtime right now. The "pattern" in the question is easily avoidable without sacrificing any performance if the inconsistency never happens, and if it does then it's unusable anyway. – Mike Marynowski Oct 31 '18 at 10:48
  • You proved my example was a bad one, as it can report "inconsistency" in a completely consistent sequence of reads and writes. I updated the question. – relatively_random Oct 31 '18 at 12:06