Why can't we lock on a value type?

C#MultithreadingLockingReference

C# Problem Overview


I was trying to lock a Boolean variable when I encountered the following error :

>'bool' is not a reference type as required by the lock statement

It seems that only reference types are allowed in lock statements, but I'm not sure I understand why.

Andreas is stating in his comment: >When [a value type] object is passed from one thread to the other, a copy is made, so the threads end up working on 2 different objects, which is safe.

Is it true? Does that mean that when I do the following, I am in fact modifying two different x in the xToTrue and the xToFalse method?

public static class Program {

    public static Boolean x = false;

    [STAThread]
    static void Main(string[] args) {

        var t = new Thread(() => xToTrue());
        t.Start();
        // ...
        xToFalse();
    }

    private static void xToTrue() {
        Program.x = true;
    }

    private static void xToFalse() {
        Program.x = false;
    }
}

(this code alone is clearly useless in its state, it is only for the example)


P.S: I know about this question on How to properly lock a value type. My question is not related to the how but to the why.

C# Solutions


Solution 1 - C#

Just a wild guess here...

but if the compiler let you lock on a value type, you would end up locking nothing at all... because each time you passed the value type to the lock, you would be passing a boxed copy of it; a different boxed copy. So the locks would be as if they were entirely different objects. (since, they actually are)

Remember that when you pass a value type for a parameter of type object, it gets boxed (wrapped) into a reference type. This makes it a brand-new object each time this happens.

Solution 2 - C#

You cannot lock a value type because it doesn't have a sync root record.

Locking is performed by CLR and OS internals mechanisms that rely upon an object having a record that can only be accessed by a single thread at a time - sync block root. Any reference type would have:

  • Pointer to a type
  • Sync block root
  • Pointer to the instance data in heap

Solution 3 - C#

It expands to:

System.Threading.Monitor.Enter(x);
try {
   ...
}
finally {
   System.Threading.Monitor.Exit(x);
}

Although they would compile, Monitor.Enter/Exit require a reference type because a value type would be boxed to a different object instance each time so each call to Enter and Exit would be operating on different objects.

From the MSDN Enter method page:

>Use Monitor to lock objects (that is, reference types), not value types. When you pass a value type variable to Enter, it is boxed as an object. If you pass the same variable to Enter again, it is boxed as a separate object, and the thread does not block. In this case, the code that Monitor is supposedly protecting is not protected. Furthermore, when you pass the variable to Exit, still another separate object is created. Because the object passed to Exit is different from the object passed to Enter, Monitor throws SynchronizationLockException. For more information, see the conceptual topic Monitors.

Solution 4 - C#

I was wondering why the .Net team decided to limit developers and allow Monitor operate on references only. First, you think it would be good to lock against a System.Int32 instead of defining a dedicated object variable just for locking purpose, these lockers don't do anything else usually.

But then it appears that any feature provided by the language must have strong semantics not just be useful for developers. So semantics with value-types is that whenever a value-type appears in code its expression is evaluated to a value. So, from semantic point of view, if we write `lock (x)' and x is a primitive value type then it's the same as we would say "lock a block of critical code agaist the value of the variable x" which sounds more than strange, for sure :). Meanwhile, when we meet ref variables in code we are used to think "Oh, it's a reference to an object" and imply that the reference can be shared between code blocks, methods, classes and even threads and processes and thus can serve as a guard.

In two words, value type variables appear in code only to be evaluated to their actual value in each and every expression - nothing more.

I guess that's one of the main points.

Solution 5 - C#

If you're asking conceptually why this isn't allowed, I would say the answer stems from the fact that a value type's identity is exactly equivalent to its value (that's what makes it a value type).

So anyone anywhere in the universe talking about the int 4 is talking about the same thing - how then can you possibly claim exclusive access to lock on it?

Solution 6 - C#

Because value types don't have the sync block that the lock statement uses to lock on an object. Only reference types carry the overhead of the type info, sync block etc.

If you box your reference type then you now have an object containing the value type and can lock on that object (I expect) since it now has the extra overhead that objects have (a pointer to a sync block that is used for locking, a pointer to the type information etc). As everyone else is stating though - if you box an object you will get a NEW object every time you box it so you will be locking on different objects every time - which completely defeats the purpose of taking a lock.

This would probably work (although it's completely pointless and I haven't tried it)

int x = 7;
object boxed = (object)x;

//thread1:
lock (boxed){
 ...
}
//thread2:
lock(boxed){
...
}

As long as everyone uses boxed and the object boxed is only set once you would probably get correct locking since you are locking on the boxed object and it's only being created once. DON'T do this though.. it's just a thought exercise (and might not even work - like I said, I haven't tested it ).

As to your second question - No, the value is not copied for each thread. Both threads will be using the same boolean, but the threads are not guaranteed to see the freshest value for it (when one thread sets the value it might not get written back to the memory location immediately, so any other thread reading the value would get an 'old' result).

Solution 7 - C#

The following is taken from MSDN:

> The lock (C#) and SyncLock (Visual Basic) statements can be used to ensure that a block of code runs to completion without interruption by other threads. This is accomplished by obtaining a mutual-exclusion lock for a given object for the duration of the code block.

and

> The argument provided to the lock keyword must be an object based on a reference type, and is used to define the scope of the lock.

I would assume that this is in part because the lock mechanism uses an instance of that object to create the mutual exclusion lock.

Solution 8 - C#

According to this MSDN Thread, the changes to a reference variable may not be visible to all the threads and they might end up using stale values, and AFAIK I think value types do make a copy when they are passed between threads.

To quote exactly from MSDN

> It's also important to clarify that the fact the assignment is atomic > does not imply that the write is immediately observed by other > threads. If the reference is not volatile, then it's possible for > another thread to read a stale value from the reference some time > after your thread has updated it. However, the update itself is > guaranteed to be atomic (you won't see a part of the underlying > pointer getting updated).

Solution 9 - C#

I think this is one of those cases where the answer to why is "because a Microsoft engineer implemented it that way".

The way locking works under the hood is by creating a table of lock structures in memory and then using the objects vtable to remember the position in the table where the required lock is. This gives the appearance that every object has a lock when in fact they don't. Only those that have been locked do. As value types don't have a reference there is no vtable to store the locks position in.

Why Microsoft chose this strange way of doing things is anyone's guess. They could have made Monitor a class you had to instantiate. I'm sure I have seen an article by an MS employee that said that on reflection this design pattern was a mistake, but I can't seem to find it now.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionOtielView Question on Stackoverflow
Solution 1 - C#Andrew BarberView Answer on Stackoverflow
Solution 2 - C#oleksiiView Answer on Stackoverflow
Solution 3 - C#George DuckettView Answer on Stackoverflow
Solution 4 - C#ArmanView Answer on Stackoverflow
Solution 5 - C#AakashMView Answer on Stackoverflow
Solution 6 - C#Russell TroywestView Answer on Stackoverflow
Solution 7 - C#ChrisBDView Answer on Stackoverflow
Solution 8 - C#VamsiView Answer on Stackoverflow
Solution 9 - C#Martin BrownView Answer on Stackoverflow