…we did not (and still do not) believe in the standard multithreading model, which is preemptive concurrency with shared memory: we still think that no one can write correct programs in a language where “a=a+1″ is not deterministic
Roberto Ierusalimschy, Luiz Henrique de Figueiredo and Waldemar Celes: The Evolution of Lua.
That’s a pretty harsh accusation, but a very good point nonetheless. Let’s see if we can write a correct multithreaded program that can add up numbers.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication5
{
class Program
{
static int a = 0;
static int b = 0;
static void Main(string[] args)
{
// technique 1
for (var i = 0; i < 100000; i++)
Task.Factory.StartNew(() => IncrementA());
// technique 2
Parallel.For(0, 100000, (i) => IncrementB());
// more than enough time for your machine to finish
Thread.Sleep(TimeSpan.FromSeconds(10));
Console.WriteLine("A is: {0}", a);
Console.WriteLine("B is: {0}", b);
Console.ReadLine();
}
static void IncrementA()
{
a = a + 1;
}
static void IncrementB()
{
b = b + 1;
}
}
}
The best thing for you to do, would be to run that on your machine and marvel at the results. However, if you’re not willing or able to, here’s the spoiler. A typical output would look something like this:
A is: 99805
B is: 45903
What on earth is going on? I’m using integers, and integers are meant to be atomically readable and writable, aren’t they? Yes, as per the spec, that is indeed the case. But it’s not what we’re doing here – we’re reading, then modifying, then writing. And as such, the nature of our multithreaded approach means we lose out. Imagine one thread reading the value of a as 0, then adding 1 to it, as another thread does the same. At the next step, they both write back the value 1 to memory, even though we’d expect the value to now be 2.
Interestingly, it looks like there’s a big difference in implementation between using the TPL, and using the Parallel class, with the Parallel approach running much faster – and therefore being much more prone to overwriting data.
The answer, if you’re not yet familiar with it, is to use the Interlocked class., which allows you to safely and atomically modify variables when many threads are potentially using them. I have trivially fixed my code sample – see below.
With the age of multicore machines truly upon us, perhaps it’s time to learn a language designed from the ground up for concurrency?
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication5
{
class Program
{
static int a = 0;
static int b = 0;
static void Main(string[] args)
{
// technique 1
for (var i = 0; i < 100000; i++)
Task.Factory.StartNew(() => IncrementA());
// technique 2
Parallel.For(0, 100000, (i) => IncrementB());
// more than enough time for your machine to finish
Thread.Sleep(TimeSpan.FromSeconds(10));
Console.WriteLine("A is: {0}", a);
Console.WriteLine("B is: {0}", b);
Console.ReadLine();
}
static void IncrementA()
{
Interlocked.Increment(ref a);
}
static void IncrementB()
{
Interlocked.Increment(ref b);
}
}
}
Recent Comments