|
Recently I've been doing a little bit of thinking about concurrency and multi-threading. Multi-threading is a fantastic tool, but it's also a really big gun with which to shoot your foot. In other words: It's a powerful tool and if it gets into the wrong (read: unprepared) hands, it can do a lot of damage. I've seen everything from multi-threading that immediately blocked, which caused the same problem it was originally intended to solve (GUI blocking while waiting for a task) to spinning off so many threads that the app spends all it's time context switching and virtually none of it's time doing work.
What I was curious about was: If I create a new System.Thread and start it, am I going to be running that thread on other cores or other processors? In short, I wanted to know if my multi-threaded apps can automatically take advantage of multi-core and multi-processor architecture without me having to do anything extra-special (or extra-painful, for that matter). Here's the code I wrote as a threading sample. It basically computes the outer edges of circles, and does it a truckload of times:
static void Main(string[] args)
{
Console.WriteLine("About to compute a lot of circles on threads.");
for (int threadCount = 0; threadCount < 2; threadCount++)
{
Thread t = new Thread(new ThreadStart(DoWork));
t.Start();
}
Console.ReadLine();
}
static void DoWork()
{
Console.WriteLine("[Thread " + Thread.CurrentThread.ManagedThreadId.ToString() + "] Start Work.");
for (int count = 0; count < 100000; count++)
{
for (int angle = 0; angle < 360; angle++)
{
double x = Math.Cos(angle);
double y = Math.Sin(angle);
}
}
Console.WriteLine("[Thread " + Thread.CurrentThread.ManagedThreadId.ToString() + "] End Work.");
}
In the code sample, I'm using 2 threads, but you can tweak it for more threads. The revealing change is when you go from 2 threads to 1 thread. On my twin-processor Xeon machine, running the above code consumed 56% of my overall CPU. In the following task manager screenshot, you'll see that my second processor did all the work while my first processor did very little. When I upped the thread count from 1 to 2 (or more), both processors engaged and the app consumes nearly all of my CPU (as would be expected).

So, to me, what this clearly shows me is that by default, if you're writing multi-threaded code in your .NET applications, you will be able to take advantage of multi-core and multi-processor architecture without having to do anything in particular. Of course, saying that is the easy part - the hard part is partitioning your application into units of work that can be done in parallel in order to really take advantage of all this new hardware coming out.
In case you're curious, here's a screenshot of the task manager looking at the CPU levels. The first spike is where I ran it on 2 threads. The second plateau is where I ran it on 4 threads with 500,000 circle computations per thread.

Wow, really cool post! Id love to hear more about this subject! Do you have
a screenshot of what happened with the multiple threads?
Jon, the second screenshot can also be found on Flickr here:
http://farm2.static.flickr.com/1188/632347133_77be94c306_o.png
Jon, I also modified the original blog post to include the second
screenshot.
great. this should make it feasible to mimic NSOperation/NSOperationQueue
in managed code.
Is it actually surprising on Windows for multiple threads to be run on
independent cores? I'd think that any modern operating system actually
schedules in terms of threads rather than tasks, using any available CPU
resources modulo some dynamic affinity calculation. That's certainly what
Mach (and therefore Mac OS X) does.
Actually, it was surprising to me because in a managed application, the CLR
host is the parent process. When your application fires up, you get an
AppDomain and everything done within the application is done within that
AppDomain (unless you're specifically requesting additional AppDomains).
Actually at the OS level, processes still rule. The creation of a thread
resource calls out to WinAPI at the process level. AppDomains are an
abstraction and aren't recognized by Windows (hence don't get context
switched or assigned affinity) - only the process in which the AppDomain
lives does, hence threads are not limited to the AppDomains in which they
originated (but are confined to the process in which the AppDomain
resides).