Once again, in this series of posts I look at the parts of the .NET Framework that may seem trivial, but can help improve your code by making it easier to write and maintain. The index of all my past little wonders posts can be found here. Last time we discussed the Interlocked class and its Add(), Increment(), and Decrement() methods which are all useful for updating a value atomically by adding (or subtracting). However, this begs the question of how do we set and read those values atomically as well? Read() – Read a value atomically Let’s begin by examining the following code: 1: public class Incrementor
2: {
3: private long _value = 0;
4:
5: public long Value { get { return _value; } }
6:
7: public void Increment()
8: {
9: Interlocked.Increment(ref _value);
10: }
11: }
12:
It uses an interlocked increment, as we discuss in my previous post (here), so we know that the increment will be thread-safe. But, to realize what’s potentially wrong we have to know a bit about how atomic reads are in 32 bit and 64 bit .NET environments.
When you are dealing with an item smaller or equal to the system word size (such as an int on a 32 bit system or a long on a 64 bit system) then the read is generally atomic, because it can grab all of the bits needed at once. However, when dealing with something larger than the system word size (reading a long on a 32 bit system for example), it cannot grab the whole value at once, which can lead to some problems since this read isn’t atomic.
For example, this means that on a 32 bit system we may read one half of the long before another thread increments the value, and the other half of it after the increment. To protect us from reading an invalid value in this manner, we can do an Interlocked.Read() to force the read to be atomic (of course, you’d want to make sure any writes or increments are atomic also):
1: public class Incrementor
2: {
3: private long _value = 0;
4:
5: public long Value
6: {
7: get { return Interlocked.Read(ref _value); }
8: }
9:
10: public void Increment()
11: {
12: Interlocked.Increment(ref _value);
13: }
14: }
Now we are guaranteed that we will read the 64 bit value atomically on a 32 bit system, thus ensuring our thread safety (assuming all other reads, writes, increments, etc. are likewise protected).
Note that as stated before, and according to the MSDN (here), it isn’t strictly necessary to use Interlocked.Read() for reading 64 bit values on 64 bit systems, but for those still working in 32 bit environments, it comes in handy when dealing with long atomically.
Exchange() – Exchanges two values atomically
Exchange() lets us store a new value in the given location (the ref parameter) and return the old value as a result. So just as Read() allows us to read atomically, one use of Exchange() is to write values atomically. For example, if we wanted to add a Reset() method to our Incrementor, we could do something like this:
1: public void Reset()
2: {
3: _value = 0;
4: }
But the assignment wouldn’t be atomic on 32 bit systems, since the word size is 32 bits and the variable is a long (64 bits). Thus our assignment could have only set half the value when a threaded read or increment happens, which would put us in a bad state.
So instead, we could write Reset() like this:
1: public void Reset()
2: {
3: Interlocked.Exchange(ref _value, 0);
4: }
And we’d be safe again on a 32 bit system.
But this isn’t the only reason Exchange() is valuable. The key comes in realizing that Exchange() doesn’t just set a new value, it returns the old as well in an atomic step. Hence the name “exchange”: you are swapping the value to set with the stored value.
So why would we want to do this? Well, anytime you want to set a value and take action based on the previous value. An example of this might be a scheme where you have several tasks, and during every so often, each of the tasks may nominate themselves to do some administrative chore. Perhaps you don’t want to make this thread dedicated for whatever reason, but want to be robust enough to let any of the threads that isn’t currently occupied nominate itself for the job. An easy and lightweight way to do this would be to have a long representing whether someone has acquired the “election” or not. So a 0 would indicate no one has been elected and 1 would indicate someone has been elected.
We could then base our nomination strategy as follows: every so often, a thread will attempt an Interlocked.Exchange() on the long and with a value of 1. The first thread to do so will set it to a 1 and return back the old value of 0. We can use this to show that they were the first to nominate and be chosen are thus “in charge”. Anyone who nominates after that will attempt the same Exchange() but will get back a value of 1, which indicates that someone already had set it to a 1 before them, thus they are not elected.
Then, the only other step we need take is to remember to release the election flag once the elected thread accomplishes its task, which we’d do by setting the value back to 0. In this way, the next thread to nominate with Exchange() will get back the 0 letting them know they are the new elected nominee.
Such code might look like this:
1: public class Nominator
2: {
3: private long _nomination = 0;
4: public bool Elect()
5: {
6: return Interlocked.Exchange(ref _nomination, 1) == 0;
7: }
8: public bool Release()
9: {
10: return Interlocked.Exchange(ref _nomination, 0) == 1;
11: }
12: }
There’s many ways to do this, of course, but you get the idea. Running 5 threads doing some “sleep” work might look like this:
1: var nominator = new Nominator();
2: var random = new Random();
3: Parallel.For(0, 5, i =>
4: {
5:
6: for (int j = 0; j < _iterations; ++j)
7: {
8: if (nominator.Elect())
9: {
10: // elected
11: Console.WriteLine("Elected nominee " + i);
12: Thread.Sleep(random.Next(100, 5000));
13: nominator.Release();
14: }
15: else
16: {
17: // not elected
18: Console.WriteLine("Did not elect nominee " + i);
19: }
20: // sleep before check again
21: Thread.Sleep(1000);
22: }
23: });
And would spit out results like:
1: Elected nominee 0
2: Did not elect nominee 2
3: Did not elect nominee 1
4: Did not elect nominee 4
5: Did not elect nominee 3
6: Did not elect nominee 3
7: Did not elect nominee 1
8: Did not elect nominee 2
9: Did not elect nominee 4
10: Elected nominee 3
11: Did not elect nominee 2
12: Did not elect nominee 1
13: Did not elect nominee 4
14: Elected nominee 0
15: Did not elect nominee 2
16: Did not elect nominee 4
17: ...
Another nice thing about the Interlocked.Exchange() is it can be used to thread-safely set pretty much anything 64 bits or less in size including references, pointers (in unsafe mode), floats, doubles, etc.
Summary
So, now we’ve seen two more things we can do with Interlocked: reading and exchanging a value atomically. Read() and Exchange() are especially valuable for reading/writing 64 bit values atomically in a 32 bit system. Exchange() has value even beyond simply atomic writes by using the Exchange() to your advantage, since it reads and set the value atomically, which allows you to do lightweight nomination systems.
There’s still a few more goodies in the Interlocked class which we’ll explore next time!
Technorati Tags: C#,CSharp,.NET,Little Wonders,Interlocked