So I left you hanging last time. I teasingly introduced three things you can do to combat thread safety problems and then didn’t adequately explain any of them. Let’s start putting that right. First I’m going to explain more about why local variables are (usually) thread safe.
The really important thing to get your head round is the difference between value types and reference types. If a variable is an instance of a value type this means that when the CPU wants to work with it, it can access the value directly. Say we have an int, that’s a value type. When the CPU wants to work with an int it can just look at it and see the value.
If it is a reference type though, the CPU can’t access the value directly, instead what it can see is a reference to where the value is held. It can then go and get the value.
Objects are always reference types.
To demonstrate, if we have a class…
class snaf { public void kung() { int foo = 10; ComplexObject bar; do some things... bar = new ComplexObject(); cookTheBooks(foo, bar); do some other things... return; } }
When we call the method kung, the variable foo and an empty reference to bar are created locally. int is a value type, whereas ComplexObject is a reference type.
Moving on a stage, we create an instance of ComplexObject and put a reference to in in bar. The actual memory used to store the new CompexObject is not local, but on the heap (which is just a big pile of memory that we have access to). After this we call another method and pass these two variables (foo and bar) in. Now we will be able to see the importance of the difference between reference types and value types, and passing by reference and passing by value.
So we call the method cookTheBooks…
private void cookTheBooks(int foo, ComplexObject bar) { foo++; bar.Total=bar.Total*cookFactor; return; }
What will happen when we return from this method back to kung? When we passed in foo it was 10. You might be surprised to know that the value of foo will still be 10, but the value of bar. Total will be 11 times what it previously was.
How? Well foo is a value type, so it was passed by value. This means that the instance of foo in cookTheBooks was actually a copy of the original foo. This means that when cookTheBooks incremented foo, it only incremented a copy of foo, not the original. When cookTheBooks returned its copy of foo was discarded and we were left in the method kung with the original foo, value still in tact.
In contrast, bar is a reference type. When we passed this to cookTheBooks what we actually passed was a copy of the reference. Spot the problem? A copy of the reference points to precisely the same location in memory as the original. So when we modify bar.Total we are modifying the original. Hopefully this diagram will make it a little clearer.
We can actually choose to pass a value type by reference (in C# by using the ref keyword). When we do this the method is not passed a copy, instead it is passed a reference to the original and any changed made in the method will change the original.
If we’d done this when we passed foo to cookTheBooks, when we incremented foo we would have incremented the original.
... cookTheBooks(ref foo, bar); return; } private void cookTheBooks(ref int foo, ComplexObject bar) { foo++; bar.Total=bar.Total*cookFactor; return; }
But we don’t want to do that here! We’ll stick to the original pass by value version.
So how does this rather long and complicated explanation affect thread safety? Well, every time kung is run, a new foo and a new reference to bar are created. Unless we explicitly pass foo by reference to something else, it’s inherently thread-safe. Nothing else can get to it.
To re-iterate – every instance of a value type that we create in a method is thread safe unless we explicitly pass it be reference somewhere else.
Member variables, even if they’re private value types, are not thread safe. If your object is used in multiple threads you have to deal with thread safety yourself.
As a reference type, bar‘s thread safety is not so straightforward. If it’s a simple object and we don’t pass it to anything, then as we have the only reference to it it must be thread safe. However, if bar is not a simple object, maybe a singleton, or maybe something that uses certain types of system resources, it might not be thread safe itself. So we have to be very careful about the assumptions we make with bar.
What impact does this have on the way we program? Well, it’s about scope. Consider this implementation…
class snaf { private int foo = 10; public void kung() { ComplexObject bar; do some things... bar = new ComplexObject(); cookTheBooks(bar); do some other things... return; } private void cookTheBooks(ComplexObject bar) { foo++; bar.Total=bar.Total*cookFactor; return; } }
Here foo is a member variable. It’s created when the class is created, not when each instance of kung is called. If two threads happen to call kung at the same time, both will try to access the same instance of foo. This could have disastrous results.
This is just one of the reasons why managing scope is important.
Next time I promise I’ll get on to locking.
The Threading Series
Article 1: Threading: Get Up To Speed Now!
Article 2: Thread Safety: Why Local Variables Are Good
Article 3: Threading: Locking and Deadlocks Introduced
Article 4: Combating Simple Deadlocks and Semaphores
Article 5: Semaphores and Message Passing