Replacing a lock with an interlocked operation

965 views Asked by At

Is there anyway of replacing this code using the Interlocked.Exchange API?

if (IsWorking == false)
{
    lock (this)
    {
        if (IsWorking == false)
        {
            IsWorking = true;
        }
    }
}
3

There are 3 answers

0
Jon Hanna On BEST ANSWER

Yes, but whether it would be useful is another matter.

You could replace isWorking with an integer, and then use Interlocked.CompareExchange(ref isWorking, 1, 0).

This is pointless though; either way the result at the end is that isWorking is 1, so we would have better concurrency by just replacing the code with IsWorking = true (or perhaps a VolatileWrite to ensure it was seen by other CPUs).

Your code is probably a reduction of something like:

if (isWorking == false)
    lock (this)
        if (isWorking == false)
        {
            DoSomethingWorthDoing();
            isWorking = true;
        }

And it's the DoSomethingWorthDoing() part that falls down.

There are ways of improving the concurrency of such double-checked locks, but it depends on a few things. One example is:

if(someUsefulThing == null)
  Interlocked.CompareExchange(ref someUsefulThing, SomeUsefulFactory(), null);

At the end of this someUsefulThing will be set to the result of SomeUsefulFactory(), and after being set it will not be set again. There will though be a period in which we might call SomeUsefulFactory() multiple times just to throw the result away. Sometimes that's a disaster, sometimes that's fine, and sometimes we could have just done no locking and been fine; it depends on just why we're concerned to share the same object here.

There are further variants, but applicability depends on just why you care about concurrency. This code, for example, implements a thread-safe concurrent dictionary using such interlocked operations, which tends to be slower than just putting a lock around accessing a dictionary when contention is low, but much better when many threads access it at the same time.

0
i3arnon On

You would usually use Interlocked.CompareExchange to do that. But unfortunatley there isn't an overload that accepts a boolean and the object and generic overloads work only on reference types. You can however hack that and use an int with only 2 values (0 and 1):

private static int IsWorking = 0;
private static void Main()
{
    var originalValue = Interlocked.CompareExchange(ref IsWorking, 1, 0);
}

Interlocked.CompareExchange, as the name suggest, compares 2 values (IsWorking and 0) and if they are equal stores the other value (1) in the original location. The return value is what used to be in the original location before invoking this atomic method. So if a 0 was returned the call replaced the value in IsWorking and if it's 1 then a different thread got there first.

About Interlocked.CompareExchange:

"If comparand and the value in location1 are equal, then value is stored in location1. Otherwise, no operation is performed. The compare and exchange operations are performed as an atomic operation. The return value of CompareExchange is the original value in location1, whether or not the exchange takes place."

0
Mike Schenk On

First of all, IsWorking = true would be equivalent unless there is other concurrent code that is holding the lock when this executes. Assignments of primitve values like that are guaranteed to be atomic. Clearly the combination of the conditional and the assigment might not be atomic. However, it appears that the code wants to set IsWorking to true only if IsWorking is now false. However, if IsWorking is now true, what would be the harm in re-setting it to True? It seems like you're missing something that wants a thread-safe way to notify the outside world of a state change. This is where you might use an Event or a Monitor.

You might also be looking for Interlocked.CompareExchange but that will only work for reference types and primitive numeric types. So you would need to change from a boolean to an int for example. However, the CompareExchange method won't wait as your lock will. It will simply return the old value regardless of whether it was replaced or not.

So if you changed IsWorking from a boolean property to an int field, you could:

int wasWorking = Interlocked.CompareExchange(ref isWorking, 1, 0);

if wasWorking is 0, you know you changed the state, if wasWorking is 1, you know you didn't change the state.