简体   繁体   中英

How to make reading this instance primitive thread-safe without locking?

The problem with the below class is when reading myThreadSafe.Value it may not return the most up-to-date value.

public class ThreadSafe
{   
    private int value;
    public int Value { get { return value; } }

    public void Update()
    {
        Interlocked.Add(ref value, 47); // UPDATE: use interlocked to not distract from the question being asked.
    }
}

I realise I could lock when reading it and writing it:

public int Value { get { lock(locker) return value; } }

public void Update()
{
    lock(locker)
    {
        value += 47;        
    }
}

And I have followed this pattern of using locks always. However I am trying to reduce the number of locks in my code (there are many and they are called frequently, I have profiled and Montior.Enter() is taking up more time then I would like - because it is called so many times).

UPDATE : I wonder now if that indeed the lock will make any difference in ensuring I am reading the most up to date value, it could still be from one of the machine's CPU caches couldn't it? (All the lock guarantees is mutual exclusive thread access).

I thought volatile would be the answer, MSDN does say: "This ensures that the most up-to-date value is present in the field at all times", however I read elsewhere write then read CPU instructions can still be swapped when using volatile in which case I could get a previous value for myThreadSafe.Value perhaps I could live with that - only being out by one update.

What is the most efficient way I will always get the most up-to-date value for myThreadSafe.Value ?

UPDATE: This code will be compiled and run on CPU Architectures:

  • x86
  • AMD64 (though I can build as x86)
  • PowerPC
  • ARM (Little-endian only)

Using the runtimes:

  • CLR v4.0
  • Mono (I am not sure of the mono runtime versions but if they correspond to the Mono versions: 3.0 at least).

I am hoping to use the same code for all builds!

OK, I believe I found the answer and my concern is vindicated!

The code happens to be thread-safe on x86 and AMD64 because they invalidate a CPUs cache when the variable is written to causing subsequent reads to read the variable from memory. to quote Shafqay Ahmed quoting Jeffrey Richter:

Since two processors can have different caches, which are copies of the ram, they can have different values. In x86 and x64 processors (according to Jeffrey's book) are designed to sync the caches of different processors so we may not see the problem.

Incidentally using lock and Interlocked flushes the variable from cache, so using lock when reading the property would have been safe. From http://blogs.msdn.com/b/ericlippert/archive/2011/06/16/atomicity-volatility-and-immutability-are-different-part-three.aspx :

Locks guarantee that memory read or modified inside the lock is observed to be consistent, locks guarantee that only one thread accesses a given hunk of memory at a time, and so on.

However there is no guarantee in the CLR specification given when reading a value updated by another thread (without using locking synchronization constructs) will be the most recent. Indeed on ARM I could well get an old value using ThreadSafe class as it is, from http://msdn.microsoft.com/en-us/magazine/jj553518.aspx :

If your code relies on lock-free algorithms that depend on the implementation of the x86 CLR (rather than the ECMA CLR specification), you'll want to add the volatile keyword to relevant variables as appropriate. Once you've marked shared state as volatile, the CLR will take care of everything for you. If you're like most developers, you're ready to run on ARM because you've already used locks to protect your shared data, properly marked volatile variables and tested your app on ARM.

So it seems the answer is I can use a lock when reading or make my field volatile , though perhaps I should use lock and try reduce the number of calls, as a man who worked on the compiler says :

The number of situations in which a lock is too slow is very small, and the probability that you are going to get the code wrong because you don't understand the exact memory model is very large. I don't attempt to write any low-lock code except for the most trivial usages of Interlocked operations. I leave the usage of "volatile" to real experts.

I'm not certain what you mean by "most up to date value". You can use locks to ensure that you don't read Value at the same time it is being written to, which may yield some oddities, but if you read it then write to it, you won't have the most up to date value.

To handle the oddities I referred to, you can use locks as you have done. But you seem to desire a different solution. If you don't want to lock the read, but you want to ensure that the write is atomic such that the read won't return an odd number or some other messy thing when doing a read during a multithreaded write, then I would recommend using the Interlocked class.

Simply:

Interlocked.Add(ref value, 47);

More Interlocked functions can be found at http://msdn.microsoft.com/en-us/library/system.threading.interlocked(v=vs.110).aspx

These functions are great when working with primitives. With more complicated objects, other solutions like ReaderWriterLockSlim and others will be needed.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM