views:

1173

answers:

7

I was reading the following article: http://msdn.microsoft.com/en-us/magazine/cc817398.aspx "Solving 11 Likely Problems In Your Multithreaded Code" by Joe Duffy

And it raised me a question: "We need to lock a .NET Int32 when reading it in a multithreaded code?"

I understand that if it was an Int64 in a 32-bit SO it could tear, as it is explained in the article. But for Int32 I imagined the following situation:

class Test
{
  private int example = 0;
  private Object thisLock = new Object();

  public void Add(int another)
  {
    lock(thisLock)
    {
      example += another;
    }
  }

  public int Read()
  {
     return example;
  }
}

I don't see a reason to include a lock in the Read method. Do you?

Update Based on the answers (by Jon Skeet and ctacke) I understand that the code above still vulnerable to multiprocessor caching (each processor has its own cache, unsynchronized with others). All the three modifications bellow fix the problem:

  1. Adding to "int example" the "volatile" property
  2. Inserting a Thread.MemoryBarrier(); before the actual reading of "int example"
  3. Read "int example" inside a "lock(thisLock)"

And I also think that "volatile" is the most elegant solution.

+1  A: 

in general, locks are only required when the value will be modified

EDIT: Mark Brackett's excellent summary is more apt:

"Locks are required when you want an otherwise non-atomic operation to be atomic"

in this case, reading a 32-bit integer on a 32-bit machine is presumably already an atomic operation... but maybe not! Perhaps the volatile keyword may be necessary.

Steven A. Lowe
Disagree. Locks are required when you want an otherwise non-atomic operation to be atomic. Since writes to a complex type are not atomic, a lock is required there - but there's loads of other scenarios that require locking as well.
Mark Brackett
@[Mark Brackett]: true, but overkill for this scenario - reading an Int32 value is already atomic on a 32-bit machine
Steven A. Lowe
Atomicity is rarely enough. If you want to make sure you see the most recent value, you need volatility too - or a lock.
Jon Skeet
Or, probably more appropriately, an interlock.
ctacke
@ctacke: For simple cases, potentially. Personally I rarely even attempt lock-free programming.
Jon Skeet
This is absolutely *not* true for > 1 processor, because part of using an atomic is it guarantees that you won't have cache coherency issues.
Paul Betts
Depends on what meaning of "atomic" you're using. You can certainly have problems with atomic non-volatile writes in .NET with multiple processors though.
Jon Skeet
@Paul: Just to clarify a bit - the word "cache" means more than just the CPU cache. The CPU may avoid cache coherency issues, but if the JIT is using a value it has cached in a register, the CPU can't deal with that.
Jon Skeet
+2  A: 

Looks fine to me.

maldevane
+3  A: 

It depends exactly how you're going to use the 32-bit number.

If you wanted to perform an operation like:

i++;

That implicitly breaks down into

  1. reading the value of i
  2. adding one
  3. storing i

If another thread modifies i after 1, but before 3, then you have a problem where i was 7, you add one to it, and now it's 492.

But if you're simply reading i, or performing a single operation, like:

i = 8;

then you don't need to lock i.

Now, your question says, "...need to lock a .NET Int32 when reading it..." but your example involves reading and then writing to an Int32.

So, it depends on what you're doing.

Ian Boyd
It depends on whether you need to see the most recent value written by thread A when reading from thread B. Usually that's desirable, which requires either locking, Interlocked.*, volatility, or explicit memory barriers.
Jon Skeet
Volatility is enough for seeing the most recent value, isn't it?
configurator
(My question is regarding reading the most recent value, not the read-increment-write, which should be done via Interlocked or (as I prefer) with a lock)
configurator
Yes it is configurator. Especially for value-type variables.
Jader Dias
+2  A: 

Having only 1 thread lock accomplishes nothing. The purpose of the lock is to block other threads, but it doesn't work if no one else checks the lock!

Now, you don't need to worry about memory corruption with a 32-bit int, because the write is atomic - but that doesn't necessarily mean you can go lock-free.

In your example, it is possible to get questionable semantics:

example = 10

Thread A:
   Add(10)
      read example (10)

Thread B:
   Read()
      read example (10)

Thread A:
      write example (10 + 10)

which means ThreadB started to read the value of example after thread A began it's update - but read the preupdated value. Whether that's a problem or not depends on what this code is supposed to do, I suppose.

Since this is example code, it may be hard to see the problem there. But, imagine the canonical counter function:

 class Counter {
    static int nextValue = 0;

    static IEnumerable<int> GetValues(int count) {
       var r = Enumerable.Range(nextValue, count);
       nextValue += count;
       return r;
    }
 }

Then, the following scenario:

 nextValue = 9;

 Thread A:
     GetValues(10)
     r = Enumerable.Range(9, 10)

 Thread B:
     GetValues(5)
     r = Enumerable.Range(9, 5)
     nextValue += 5 (now equals 14)

 Thread A:
     nextValue += 10 (now equals 24)

The nextValue is incremented properly, but the ranges returned will overlap. The values of 19 - 24 were never returned. You would fix this by locking around the var r and nextValue assignment to prevent any other thread from executing at the same time.

Mark Brackett
This is a classic case for an interlock.
ctacke
+14  A: 

Locking accomplishes two things:

  • It acts as a mutex, so you can make sure only one thread modifies a set of values at a time.
  • It provides memory barriers (acquire/release semantics) which ensures that memory writes made by one thread are visible in another.

Most people understand the first point, but not the second. Suppose you used the code in the question from two different threads, with one thread calling Add repeatedly and another thread calling Read. Atomicity on its own would ensure that you only ended up reading a multiple of 8 - and if there were two threads calling Add your lock would ensure that you didn't "lose" any additions. However, it's quite possible that your Read thread would only ever read 0, even after Add had been called several times. Without any memory barriers, the JIT could just cache the value in a register and assume it hadn't changed between reads. The point of a memory barrier is to either make sure something is really written to main memory, or really read from main memory.

Memory models can get pretty hairy, but if you follow the simple rule of taking out a lock every time you want to access shared data (for read or write) you'll be okay. See the volatility/atomicity part of my threading tutorial for more details.

Jon Skeet
You're saying that I have to read the Int32 inside a lock or that I have to insert a Thread.MemoryBarrier(); before the actual reading?
Jader Dias
@Vernicht: Yes. Or preferably use Interlocked, a volatile variable, or a lock. Using Thread.MemoryBarrier should really be a last resort.
Jon Skeet
@Jon: The volatile keyword solves problems caused by both types of caching (CPU and JIT)?
Jader Dias
@Vernicht: Yes, but it doesn't help for things like incrementing a variable - it doesn't make those three operations (read, increment, write) atomic.
Jon Skeet
@Jon: the documentation for Thread.MemoryBarrier() says nothing about the visibility part, only the re-ordering: http://msdn.microsoft.com/en-us/library/system.threading.thread.memorybarrier.aspx. So, I'm not very sure about your assessment that "The point of a memory barrier is to either make sure something is really written to main memory, or really read from main memory".
Buu Nguyen
@Buu: Reordering *is* about visibility, effectively. It can be described in various ways, but that really is what it's about. Unfortunately the whole topic is confusing and not terribly well explained in documentation :(
Jon Skeet
@Jon: I've just read Joseph Albahari's article, which states "The simplest kind of memory barrier is a full memory barrier (full fence) which prevents any kind of instruction reordering or caching *around* that fence". I understand it that if we have some code like "S0; barrier; S1;" then *both* S0 and S1 always write to or read from *main memory*. (It's not clear how far ahead the barrier reaches though, I suppose 1 instruction here.) Given your answer about the implementation of Thread.VolatileRead() in another post, I'm interested in knowing what you think about the statement.
Buu Nguyen
@Jon: the "another post" I referred to in my previous comment is: http://stackoverflow.com/questions/1773680/thread-volatileread-implementation/1773696#1773696 . And the link to Joseph Albahari's article http://www.albahari.com/threading/part4.aspx .
Buu Nguyen
@Buu: Frankly, volatility and memory fences scare me enough that I'd rather not make too many statements about them :) Lock-free threading (with mutation) is nasty stuff. At one time I thought I understood it... not any more.
Jon Skeet
@Jon: sadly, I agree with you. My mental model about volatility yet, the trend doesn't seem to stop. After a lot of reading, including Brian Goetze's and Joe Duffy's books, I feel bad that I am still not able to confidently write lock-free code :(.
Buu Nguyen
+5  A: 

It all depends on the context. When dealing with integral types or references you might want to use members of the System.Threading.Interlocked class.

A typical usage like:

if( x == null )
  x = new X();

Can be replaced with a call to Interlocked.CompareExchange():

Interlocked.CompareExchange( ref x, new X(), null);

Interlocked.CompareExchange() guarantees that the comparison and exchange happen as an atomic operation.

Other members of the Interlocked class, such as Add(), Decrement(), Exchange(), Increment() and Read() all perform their respective operations atomically. Read the documentation on MSDN.

guardi
Just wanted to note that this example is not recommended if the constructor has any non-trivial amount of logic, since it would call the constructor every time you call that line - and not only when it's needed.
configurator
You are right. In my code I always surround the call to CompareExchange with an if. Since this doesn't guarantee single execution for heavyweight constructors or factory methods, I got into the habit of using LazyInit<> from the Parallel Extensions Library.
guardi
+1  A: 

Locking is necessary if you need it to be atomic. Reading and writing (as a paired operation, such as when you do an i++) a 32-bit number is not guaranteed to be atomic due to caching. In addition an individual read or write doesn't necessarily go right to a register (volatility). Making it volatile doesn't give you any guarantee of atomicity if you have the desire to modify the integer (e.g. a read, increment, write operation). For integers a mutex or monitor may be too heavy (depends on your use case) and that's what the Interlocked class is for. It guarantees atomicity of these types of operation.

ctacke
32 bit reads and writes are guaranteed to be atomic, but not volatile. They're different things.
Jon Skeet
(Guarantee of atomicity is in ECMA 335 partition 1 section 12.6.6.)
Jon Skeet
I've clarified my poorly worded response, though it definitely deserves more attention that it's atomic *only* when properly aligned and when *all* access to the location are of the same size. Either of these come into question when using unsafe code.
ctacke
Agreed about unsafe code - but the alignment etc is guaranteed when you don't have any explicit layout (and for values of the appropriate size, of course).
Jon Skeet
ctacke, your answer has helped me
Jader Dias