Often times, we need to update a count in a multi-threaded program. This may be an incrementing, decrementing, or adding a value in a thread-safe manner.
private static int _tradeCount = 0;
static void Main(string[] args)
{
Stopwatch timer = new Stopwatch();
timer.Start();
/*8 Processor in my C.P.U*/
Parallel.For(0, Environment.ProcessorCount, j =>
{
for (int k = 0; k < 100000000; ++k)
{
IncrementTrades();
}
}); timer.Stop();
Console.WriteLine(string.Format("Incremented total trades {0:#,##0} in {1} ms", _tradeCount, timer.ElapsedMilliseconds));
}
public static void IncrementTrades()
{
++_tradeCount;
}
My system has 8 processors, and instead of getting an expected number of 800,000,000 (100 million per thread), I got: 118,668,575
The thing to realize is that the increment operator is not guaranteed to be thread-safe, basically it reads the current value, increments it, then stores result back into the variable. In between those steps there is no protection, so it’s possible for two threads to load the current value (say 113), both of them increment it (say to 114) and then both store 114. Thus, we’ve missed an increment because they both read the same value and made the same increment, instead of synchronizing their access and update.
static readonly object _mutex = new object();
public static void IncrementTrades()
{
lock (_mutex)
{
++_tradeCount;
}
}
This is heavier (again because of the contention for the atomic operation), but it’s still much lighter (about 40% less overhead) than the full lock.
This post will discuss one of the lightest ways to do this: the Interlocked class.
Problem: Increments Are Not Thread-Safe
For example, if wanted to increment a count of trades in a system, we could do this:
private static int _tradeCount = 0;
static void Main(string[] args)
{
Stopwatch timer = new Stopwatch();
timer.Start();
/*8 Processor in my C.P.U*/
Parallel.For(0, Environment.ProcessorCount, j =>
{
for (int k = 0; k < 100000000; ++k)
{
IncrementTrades();
}
}); timer.Stop();
Console.WriteLine(string.Format("Incremented total trades {0:#,##0} in {1} ms", _tradeCount, timer.ElapsedMilliseconds));
}
public static void IncrementTrades()
{
++_tradeCount;
}
Output
Incremented total trades 118,668,575 in 5230 msMy system has 8 processors, and instead of getting an expected number of 800,000,000 (100 million per thread), I got: 118,668,575
The thing to realize is that the increment operator is not guaranteed to be thread-safe, basically it reads the current value, increments it, then stores result back into the variable. In between those steps there is no protection, so it’s possible for two threads to load the current value (say 113), both of them increment it (say to 114) and then both store 114. Thus, we’ve missed an increment because they both read the same value and made the same increment, instead of synchronizing their access and update.
Locking – The Typical First Choice (but costly)
static readonly object _mutex = new object();
public static void IncrementTrades()
{
lock (_mutex)
{
++_tradeCount;
}
}
Output
Incremented total trades 800,000,000 in 39,461 ms
Well, at least we now have the right answer, but we also went up an order of magnitude in single-threaded performance, and nearly two orders of magnitude in multi-threaded performance. Some of this is to be expected, because we’re losing some of our concurrency in the process of protecting the increment. Since the only thing going on in these four threads is fighting for the lock, it will take longer, but the cost increase for a single-thread alone is quite a big jump.
But there’s a lighter solution than locking for simple numeric addition and set operations in a thread-safe manner.
Interlocked – A Better Choice (but limited)
As long as we limit ourselves to talking about simple addition/subtraction (increment, decrement, add, etc.) on and int or long field, there is a lighter solution: the Interlocked class.
The Interlocked class works by requesting that the CLR and Operating System increment (or, decrement, etc.) the number atomically. The .NET Framework’s Interlocked class taps right into this OS functionality, thus allowing you to avoid errors when two threads attempt an increment at the same time.
Lets compares the performance outcomes by comparing conventional lock and far better choice Interlocked class.
public static void IncrementTrades()
{
Interlocked.Increment(ref _tradeCount);
}
{
Interlocked.Increment(ref _tradeCount);
}
Output
Incremented total trades 800,000,000 in 14,984 msThis is heavier (again because of the contention for the atomic operation), but it’s still much lighter (about 40% less overhead) than the full lock.
Remarks
Using the C# increment operator (++) is not a thread-safe operation, thus if you are wanting to keep an accurate count across multiple threads in a concurrent environment you need to synchronize the update to the count.
One of the lightest form of synchronization for simple increments is the Interlocked.Increment() and it has similar sister methods for decrementing, adding, and subtracting values.
No comments:
Post a Comment