Is Interlocked.CompareExchange really faster than a simple lock?
Asked Answered
C

7

12

I came across a ConcurrentDictionary implementation for .NET 3.5 (I'm so sorry I could find the link right now) that uses this approach for locking:

var current = Thread.CurrentThread.ManagedThreadId;
while (Interlocked.CompareExchange(ref owner, current, 0) != current) { }

// PROCESS SOMETHING HERE

if (current != Interlocked.Exchange(ref owner, 0))
        throw new UnauthorizedAccessException("Thread had access to cache even though it shouldn't have.");

Instead of the traditional lock:

lock(lockObject)
{
    // PROCESS SOMETHING HERE
}

The question is: Is there any real reason for doing this? Is it faster or have some hidden benefit?

PS: I know there's a ConcurrentDictionary in some latest version of .NET but I can't use for a legacy project.

Edit:

In my specific case, what I'm doing is just manipulating an internal Dictionary class in such a way that it's thread safe.

Example:

public bool RemoveItem(TKey key)
{
    // open lock
    var current = Thread.CurrentThread.ManagedThreadId;
    while (Interlocked.CompareExchange(ref owner, current, 0) != current) { }


    // real processing starts here (entries is a regular `Dictionary` class.
    var found = entries.Remove(key);


    // verify lock
    if (current != Interlocked.Exchange(ref owner, 0))
        throw new UnauthorizedAccessException("Thread had access to cache even though it shouldn't have.");
    return found;
}

As @doctorlove suggested, this is the code: https://github.com/miensol/SimpleConfigSections/blob/master/SimpleConfigSections/Cache.cs

Corroboree answered 27/12, 2013 at 12:44 Comment(3)
Can you elaborate more do you need the whole section to get synchronized as you said // PROCESS SOMETHING HERE or you just want to exchange values atomically.....Uncommitted
@dbw, I edited for more informationCorroboree
Is it this code? github.com/miensol/SimpleConfigSections/blob/master/…Amphioxus
S
8

Your CompareExchange sample code doesn't release the lock if an exception is thrown by "PROCESS SOMETHING HERE".

For this reason as well as the simpler, more readable code, I would prefer the lock statement.

You could rectify the problem with a try/finally, but this makes the code even uglier.

The linked ConcurrentDictionary implementation has a bug: it will fail to release the lock if the caller passes a null key, potentially leaving other threads spinning indefinitely.

As for efficiency, your CompareExchange version is essentially a Spinlock, which can be efficient if threads are only likely to be blocked for short periods. But inserting into a managed dictionary can take a relatively long time, since it may be necessary to resize the dictionary. Therefore, IMHO, this isn't a good candidate for a spinlock - which can be wasteful, especially on single-processor system.

Simplism answered 27/12, 2013 at 13:8 Comment(1)
I agree. Interlocked may result in better performance but it's less readable and more bug prone. So unless you trust that source I would go with a simple lock.Gravettian
K
8

There is no definitive answer to your question. I would answer: it depends.

What the code you've provided is doing is:

  1. wait for an object to be in a known state (threadId == 0 == no current work)
  2. do work
  3. set back the known state to the object
  4. another thread now can do work too, because it can go from step 1 to step 2

As you've noted, you have a loop in the code that actually does the "wait" step. You don't block the thread until you can access to your critical section, you just burn CPU instead. Try to replace your processing (in your case, a call to Remove) by Thread.Sleep(2000), you'll see the other "waiting" thread consuming all of one of your CPUs for 2s in the loop.

Which means, which one is better depends on several factors. For example: how many concurrent accesses are there? How long the operation takes to complete? How many CPUs do you have?

I would use lock instead of Interlocked because it's way easier to read and maintain. The exception would be the case you've got a piece of code called millions of times, and a particular use case you're sure Interlocked is faster.

So you'll have to measure by yourself both approaches. If you don't have time for this, then you probably don't need to worry about performances, and you should use lock.

Kowal answered 27/12, 2013 at 13:24 Comment(0)
C
5

A little bit late... I have read your sample but in short:

Fastest to slowest MT sync:

  • Interlocked.* => This is a CPU atomic instruction. Can't be beat if it is sufficient for your need.
  • SpinLock => Uses Interlocked behind and is really fast. Uses CPU when wait. Do not use for code that wait long time (it is usually used to prevent thread switching for lock that do quick action). If you often have to wait more than one thread cycle, I would suggest to go with "Lock"
  • Lock => The slowest but easier to use and read than SpinLock. The instruction itself is very fast but if it can't acquire the lock it will relinquish the cpu. Behind the scene, it will do a WaitForSingleObject on a kernel objet (CriticalSection) and then Window will give cpu time to the thread only when the lock will be freed by the thread that acquired it.

Have fun with MT!

Chiton answered 13/6, 2017 at 20:50 Comment(0)
G
3

Yes. The Interlocked class offer atomic operations which means they do not block other code like a lock because they don't really need to. When you lock a block of code you want to make sure no 2 threads are in it at the same time, that means that when a thread is inside all other threads wait to get in, which uses resources (cpu time and idle threads). The atomic operations on the other hand do not need to block other atomic operations because they are atomic. It's conceptually a one CPU operation, the next ones just go in after the previous, and you're not wasting threads on just waiting. (By the way, that's why it's limited to very basic operations like Increment, Exchange etc.)

I think a lock (which is a Monitor underneath) uses interlocked to know if the lock is already taken, but it can't know that the actions inside it can be atomic.

In most cases, though, the difference is not critical. But you need to verify that for your specific case.

Gravettian answered 27/12, 2013 at 12:47 Comment(3)
Thank you, but can you please elaborate a little bit more?Corroboree
I also added a code sample for more information on my specific case.Corroboree
I might be wrong but it seems the code snippet you added uses interlocked to verify no other thread changed the value while you were inside the operation. That isn't really "blocking" other threads, but more like trying an operation and failing if it was concurrent with other calls.Gravettian
A
3

The docs for the Interlocked class tell us it

"Provides atomic operations for variables that are shared by multiple threads. "

The theory is an atomic operation can be faster than locks. Albahari gives some further details on interlocked operations stating they are faster.

Note that Interlocked provides a "smaller" interface than Lock - see previous question here

Amphioxus answered 27/12, 2013 at 13:4 Comment(0)
U
1

Interlocked is faster - already explained in other comments and you can also define the logic of how the wait is implemented e.g. spinWait.spin(), spinUntil, Thread.sleep etc once the lock fails the first time.. Also, if your code within the lock is expected to run without possibility of crash (custom code/delegates/resource resolution or allocation/events/unexpected code executed during the lock) unless you are going to be catching the exception to allow your software to continue execution, "try" "finally" is also skipped so extra speed there. lock(something) makes sure if you catch the exception from outside to unlock that something, just like "using" makes sure (C#) when the execution exits the execution block for whatever reason to dispose the "used" disposable object.

Unvoiced answered 2/1, 2020 at 23:45 Comment(0)
M
1

One important difference between lock and interlock.CompareExhange is how it can be used in async environments.

async operations cannot be awaited inside a lock, as they can easily occur in deadlocks if the thread that continues execution after the await is not the same one that originally acquired the lock.

This is not a problem with interlocked however, because nothing is "acquired" by a thread.

Another solution for asynchronous code that may provide better readability than interlocked may be semaphore as described in this blog post: https://blog.cdemi.io/async-waiting-inside-c-sharp-locks/

Mincemeat answered 19/2, 2021 at 19:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.