We need to lock a .NET Int32 when reading it in a multithreaded code?

大憨熊 提交于 2019-12-18 10:29:16

问题


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:


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.




回答2:


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.




回答3:


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.




回答4:


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.




回答5:


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.




回答6:


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.



来源:https://stackoverflow.com/questions/395232/we-need-to-lock-a-net-int32-when-reading-it-in-a-multithreaded-code

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!