1

I got a book about programming in C#. There is an example. Let me copy and paste the context.

...Introduced the usage of Interlocked(omitted here).

You can also use the CompareExchange method. This method first checks to see whether the expected value is there; if it is, it replaces it with another value.

Listing 1-41 shows what can go wrong when comparing and exchanging a value in a nonatomic operation.

LISTING 1-41 Compare and exchange as a nonatomic operation

class Program
{
    static int value = 1;
    static void Main(string[] args)
    {
        Task t1 = Task.Run(() =>
            {
                if(value==1)
                {
                    // Removing the following line will change the output
                    Thread.Sleep(1000);
                    value = 2;
                }
            });
        Task t2 = Task.Run(() =>
        {
            value = 3;
        });
        Task.WaitAll(t1, t2); // Displays 2
        Console.WriteLine(value);
    }
}

Task t1 starts running and sees that value is equal to 1. At the same time, t2 changes the value to 3 and then t1 changes it back to 2. To avoid this, you can use the following Interlocked statement:

Interlocked.CompareExchange(ref value, newValue, compareTo);

This makes sure that comparing the value and exchanging it for a new one is an atomic operation. This way, no other thread can change the value between comparing and exchanging it.

The book doesn't provide further details and just ends this section at this point.

My question is how to apply Interlocked.CompareExchange(ref value, newValue, compareTo); to the code?

Hui Zhao
  • 655
  • 1
  • 10
  • 20

4 Answers4

4

Well, I guess they mean to replace the non-atomic block:

if(value==1)
{
    // Removing the following line will change the output
    Thread.Sleep(1000);
    value = 2;
}

With

Interlocked.CompareExchange(ref value, 2, 1);

Edit, what problem does Interlocked.CompareExchange solve?

The two tasks, t1 and t2 both change value, potentially concurrently.

This code sequence here a common anti-pattern in multi threaded code - the ABA problem:

if(someUnsynchronizedCondition)
{
    ... Other random code here
    doSomeActionDependentOnTheCheckedCondition();
}

The problem with this is that the condition which was checked for (if (value==1)) is not necessarily true any longer at the point where the dependent action is taken (value = 2; in your case), because other threads can change the variable in the same time it takes for this thread to move from the if check to the dependent action (obviously, the more time the "other random code" takes, like a 2 second sleep, the more the risk). But there is always risk, even if there is no extra code in between (i.e. the original code is still prone to ABA even if Thread.Sleep is removed entirely).

In the book's example, the action is to mutate the same variable used in the condition, so hence the convenient way of doing this with Interlocked.CompareExchange - for Intel processors, this translates to a single lock cmpxchg [reg1], reg2 atomic instruction.

In a more general sense (i.e. where the dependent action isn't just to change the checked variable), you'll need to use other means of synchronization, such as a lock or ReaderWriterLockSlim, especially if there is a bias toward more readers than writers.

Re : Final result

Yes, the code will now always show 3, simply because if the first Task t1 executes first, it will change the value to 2 which will be overruled by the second Task t2 setting it to 3, but if the second Task runs first (setting to 3), then t1 won't update the value because it is 3, not 1.

Community
  • 1
  • 1
StuartLC
  • 104,537
  • 17
  • 209
  • 285
3

The Interlocked.CompareExchange(ref value, newValue, compareTo); basically does this:

if(value==compareTo)
{
    value = newValue;
}

But in an atomic, in other words, non-interruptable fashion. This prevents the problem you have in the example code where you check the value, but then the value changes before you assign it a new value. This can be very important in multithreaded code where you have shared variables.

There are other techniques you might see for this kind of thing. For example, using an object to lock:

lock(someObject) 
{
    if(value==compareTo)
    {
        value = newValue;
    }
}

But this will only work if every other attempt to change value locks on the same object.

Matt Burland
  • 44,552
  • 18
  • 99
  • 171
2

It replaces the separate compare and exchange instructions. In this block from t1:

if(value==1) //Compare
{
    Thread.Sleep(1000);
    value = 2; //Exchange
}

Hopefully you will have seen that while t1 is asleep, t2 can cut in and set the value to 3 because your compare and exchange are separate - i.e. not atomic.

If you replace that whole block with:

Interlocked.CompareExchange(ref value, 2, 1);

Then the runtime guarantees that the compare and exchange are done together and no other thread can cut in the middle of those operations.

It's not a great example because you also necessarily lose the Thread.sleep which makes it less intuitive to follow IMHO.

Paolo
  • 22,188
  • 6
  • 42
  • 49
0
class Program
{
    static int value = 1;
    static void Main(string[] args)
    {
        Task t1 = Task.Run(() =>
        {
            Interlocked.CompareExchange(ref value, 2, 1);
        });
        Task t2 = Task.Run(() =>
        {
            value = 3;
        });
        Task.WaitAll(t1, t2);
        Console.WriteLine(value);
    }
}
PiotrWolkowski
  • 8,408
  • 6
  • 48
  • 68