Why can't we lock on a value type?
Asked Answered
S

9

69

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.

Sherly answered 25/11, 2011 at 9:58 Comment(2)
Your program is not passing x between the threads it is using a shared copy. However it is still not thread safe as you are not accessing x in the scope of a lock and x is not declared volatile. I feel another question comming on: "Why does x in this example need to be volatile?"Knavish
@MartinBrown: I know, and in my real code, I use a lock on a dedicated object (as I mentioned in my question, it is related to the why and not the how). As for volatile, it is not needed when locking properly.Sherly
W
56

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.

Washbasin answered 25/11, 2011 at 10:4 Comment(3)
The key point is that the value type would be boxed into a different object each time (boxing isn't quite the same as copying and i think for the OP and future readers it's worth noting). See my answerSwetiana
The compiler could create an invisible fixed reference type when locking value types.Gillispie
Does that men that in csharp, unlike Java, bool and Boolean are the same? That's annoying.Keele
C
30

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
Chest answered 27/11, 2011 at 21:15 Comment(0)
S
24

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.

Swetiana answered 25/11, 2011 at 10:0 Comment(4)
And why do Monitor.Enter and Monitor.Exit require a reference type? (seems like the obvious question to ask, seeing as that's really what the OP is after).Hegelian
Not when checked by the compiler it doesn't... It may require a reference type to work properly, and it might even throw an exception if the value is a boxed value type I haven't checked. But it compiles just fine. The reference type check done by the compiler is only for the lock() statement.Samarskite
@AndersForsgren: Edited to clarify.Swetiana
It actually expands to have the enter inside the try{} these days too. Sorry, just nitpicking now.Samarskite
K
8

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?

Kistner answered 25/11, 2011 at 10:8 Comment(1)
That's an interesting point; particularly if someone asks, "so since a value type gets boxed when passed to an object param, why not have lock also accept specific value types in addition?"... because to do that, the lock would be on the value - not the variable. That would be useless. +1Washbasin
J
7

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.

Jeneejenei answered 7/11, 2013 at 9:41 Comment(2)
+1 Explained simply, and you've answered @martin brown as wellFade
Maybe something like lock(ref value) can make sense?Byandby
V
2

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).

Vergos answered 25/11, 2011 at 10:1 Comment(1)
Thanks for answering my second question. That sounded weird to me that values are copied for each thread.Sherly
L
1

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.

Lacrimator answered 25/11, 2011 at 10:7 Comment(0)
K
1

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).

Kalpak answered 25/11, 2011 at 10:9 Comment(1)
The value type isn't copied when used by different threads. It's the same address in memry - the reason you might get stale values is that the value you set in one thread might not be immediately written back out to the memory location - it might be 'cached' in a register because the JIT can see it's going to be used again soon.Vergos
K
0

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.

Knavish answered 25/11, 2011 at 14:51 Comment(1)
I believe the reason Microsoft did things that way was that an instantiable lock type would either: (1) require a finalizer, (2) leak resources if entered but not left, or (3) require some other GC support for cleanup if abandoned. My guess would be that MS decided on approach #3, and implemented it in such a way that every class object would have the cost imposed equally, and thus there was no technical impediment to allowing locking on every class object.Lenzi

© 2022 - 2024 — McMap. All rights reserved.