Channels ▼

Gastón Hillar

Dr. Dobb's Bloggers

Silverlight 4 Makes it Easy to Count Logical Cores

December 10, 2010

Silverlight 4 allows you to access the System.Environment.ProcessorCount property to get the number of logical cores, also known as hardware threads, on the current machine. In previous Silverlight versions, it was difficult to retrieve the number of logical cores because this property requires special permissions to retrieve this value. Silverlight 4 solved this problem because it allows you to access this property in any Silverlight application and you can easily launch as many threads as the number of available logical cores.

In "Silverlight 4 RC Stays With the Old .NET Threads; F# Helps", I explained that it was necessary to work with the old .NET multithreading model if you wanted your Silverlight 4 applications to take advantage of multiple cores. Unluckily, Silverlight 4 doesn't provide support for Parallel Extensions. However, you can take into account the value of the System.Environment.ProcessorCount property to distribute work in different threads.

I usually receive many e-mails with questions about threads and Silverlight. One of the most common questions I receive is: "Are Silverlight threads real threads that can take advantage of multiple cores?" The answer is yes, Silverlight allows you to work with managed threads. If you run code in different managed threads, the underlying operating system threads will usually distribute the execution of this code on the different available cores. A very simple example will allow you to test the possibilities offered by Silverlight 4 in diverse multicore machines.

The following XAML code snippet (MainPage.xaml) defines a very simple UI with two Button controls, a Label and a TextBox. Figure 1 shows the design view for MainPage.xaml.


<UserControl x:Class="SilverlightThreads.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">

    <Grid x:Name="LayoutRoot" Background="White">
        <Button Content="Use 1 Thread" Height="23" HorizontalAlignment="Left" Margin="12,12,0,0" Name="butRunInOneThread" VerticalAlignment="Top" Width="156" Click="butRunInOneThread_Click" />
        <Button Content="Use Multiple Threads" Height="23" HorizontalAlignment="Left" Margin="12,62,0,0" Name="butRunInMultipleThreads" VerticalAlignment="Top" Width="156" Click="butRunInMultipleThreads_Click" />
        <sdk:Label Height="51" HorizontalAlignment="Left" Margin="12,111,0,0" Name="lblResult" VerticalAlignment="Top" Width="376" Content="Click on the desired button" AllowDrop="False" />
        <TextBox Height="120" HorizontalAlignment="Left" Margin="12,168,0,0" Name="textBox1" VerticalAlignment="Top" Width="376" />
    </Grid>
</UserControl>

Figure 1. The design view for MainPage.xaml.

The following C# code snippet (MainPage.xaml.cs) defines the Click event handlers for butRunInOneThread and butRunInMultipleThreads. In addition, the code snippet defines four private variables and the StaticPartitioningParallelFor method. The code isn't decoupled from the UI and doesn't represent a best practice. Remember that this is a very simple example to enable you to test how Silverlight threads take advantage of multiple cores.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Threading;
using System.Diagnostics;

namespace SilverlightThreads
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
        }

        // Work with 50,000,000 numbers
        private const int TOTAL_NUMBERS = 50000000;
        // The resulting list of hex strings
        private List<string> _hexStrings;
        // An object to synchronize access to _sum
        private object _syncSum = new object();
        // The final sum
        private double _sum;

        private void StaticPartitioningParallelFor(
            int numThreads)
        {
            var startTick = Environment.TickCount;

            _hexStrings = new List<string>();
            _sum = 0;

            // Calculate the range
            int range = TOTAL_NUMBERS / numThreads;
            // The number of threads that have to be created
            int remainingThreads = numThreads;
            using (ManualResetEvent mre = new ManualResetEvent(false))
            {
                for (int threadNumber = 0; threadNumber < numThreads; threadNumber++)
                {
                    // Calculate the value to start the serial loop (inclusive)
                    int startNumber = 
                        threadNumber * range;
                    // Calculate the value to end the serial loop (exclusive)
                    int endNumber = 
                        (threadNumber == (numThreads - 1)) ?
                        TOTAL_NUMBERS : startNumber + range;
                    // Ask the ThreadPool to queue a new work item
                    ThreadPool.QueueUserWorkItem(delegate
                    {
                        // This delegate runs in a new thread

                        // Write information about the range that will be processed
                        Debug.WriteLine(
                            "From (inclusive): {0} To (exclusive) {1}", startNumber, endNumber);

                        // Create a local list
                        var localList = new List<string>(endNumber - startNumber);
                        double localsum = 0;
                        for (int value = startNumber; value < endNumber; value++)
                        {
                            // Consume some CPU cycles
                            string hexString = 
                                String.Format("{0,20:X}", value).Trim().ToLower();
                            if ((hexString[0] == 'a'))
                            {
                                localList.Add(hexString);
                                localsum += hexString[0];
                            }
                            else if ((hexString[0] == 'f') || (hexString[0] == '9'))
                            {
                                localList.Add(hexString);
                                localsum -= hexString[0];
                            }
                        }

                        lock (_hexStrings)
                        {
                            // Critical section
                            // Add the local list to _hexString
                            _hexStrings.AddRange(localList);
                        }
                        lock (_syncSum)
                        {
                            // Critical section
                            // Add localsum to _sum
                            _sum += localsum;
                        }

                        if (Interlocked.Decrement(ref remainingThreads) == 0)
                        {
                            // Signal/set the ManualResetEvent
                            mre.Set();
                        }
                    });
                }
                // Wait for all the threads to complete their job
                mre.WaitOne();

                Dispatcher.BeginInvoke(() =>
                {
                    // This code will run in the UI thread
                    lblResult.Content =
                        String.Format(
                            "Sum: {0}, Items added: {1}, \nElapsed time: {2} milliseconds",
                            _sum,
                            _hexStrings.Count,
                            ((Environment.TickCount - startTick) /
                             (double)TimeSpan.TicksPerMillisecond) * 10000);
                    butRunInMultipleThreads.IsEnabled = true;
                    butRunInOneThread.IsEnabled = true;
                });
            }
        }

        private void butRunInOneThread_Click(object sender, RoutedEventArgs e)
        {
            butRunInMultipleThreads.IsEnabled = false;
            butRunInOneThread.IsEnabled = false;
            ThreadPool.QueueUserWorkItem(delegate
            {
                StaticPartitioningParallelFor(1);
            });
        }

        private void butRunInMultipleThreads_Click(object sender, RoutedEventArgs e)
        {
            butRunInMultipleThreads.IsEnabled = false;
            butRunInOneThread.IsEnabled = false;
            ThreadPool.QueueUserWorkItem(delegate
            {
                StaticPartitioningParallelFor(Environment.ProcessorCount);
            });
        }
    }
}

If you click on the "Use 1 Thread" button (butRunInOneThread), the code defined in the event handler disables the two Button controls and calls the ThreadPool.QueueUserWorkItem method to queue a work item in the thread pool. The delegate that defines this work item will run in a background thread that is different from the UI thread, and therefore, the UI thread will be free while the code runs in a different thread. The delegate calls StaticPartitioningParallelFor(1). The StaticPartitioningParallelFor method receives the desired number of threads (numThreads) as a parameter. In this case, numThreads is 1, and therefore, the method is going to create only one thread. You can click on the "Use 1 Thread" button to measure the time is takes to run the algorithm when the code uses only one thread. The algorithm will display the time required to complete the job when it finishes. In the meantime, you will be able to use the TextBox control to write some text because the UI thread is free.

If you click on the "Use Multiple Threads" button (butRunInMultipleThreads), the code defined in the event handler disables the two Button controls and calls the ThreadPool.QueueUserWorkItem method to queue a work item in the thread pool. As explained for the other button, the delegate that defines this work item will run in a background thread that is different from the UI thread, and therefore, the UI thread will be free while the code runs in a different thread. The delegate calls StaticPartitioningParallelFor(Environment.ProcessorCount). In this case, numThreads is equal to the number of logical cores or hardware threads, and therefore, the method is going to create as many threads as the number of available logical cores. For example, if the machine has a quad-core CPU with Hyper-Threading, the StaticPartitioningParallelFor method is going to create eight threads because there are eight logical cores available.

The StaticPartitioningParallelFor method employs a static partitioning. The method partitions the dataset in many parts and distributes the workload among different threads. This method doesn't include load-balancing mechanisms and could lead to a load imbalance problem. However, the method is useful for this simple example because we cannot use the new Parallel.For with the load-balancing capabilities, included in .NET Framework 4.

The StaticPartitioningParallelFor method calls ThreadPool.QueueUserWorkItem for each part that has to be processed in a different thread. A sequential loop processes each dataset in a different thread and then the code collects the data in a critical section. The code included in the sequential loop consumes some CPU cycles and doesn't represent the most efficient algorithm. The goal is to consume some CPU cycles and to compare the results of running the code with diverse numbers of cores.

The code uses a ManualResetEvent to control whether all the threads have finished their job. Of course, the code would require additional lines to handle potential exceptions. When all the threads finish their job and the final results are computed, a call to Dispatcher.BeginInvoke queues a delegate to run in the UI thread. This delegate shows the results in a Label control.

If you run this example in a machine with a multicore microprocessor, you will notice an important speedup when you click on the "Use Multiple Threads" button. Remember to disable turbo modes, such as Intel Turbo boost, because these turbo modes usually distort your speedup measurements. If you have doubts about the distortions in speedup measurement, you can read "Measuring Speedup is Challenging with Intel Turbo Boost Technology."

In a computer with a quad-core CPU, the code that uses multiple threads provides a 3.15x speedup over the single-threaded version. Silverlight creates real threads that take advantage of multiple cores. I hope that Microsoft realizes that it would be great to have Parallel Extensions on Silverlight soon. Balder, an open source and free 3D engine, and SilverMotion, a commercial 3D engine, are two excellent examples of Silverlight engines that were optimized to take advantage of multicore microprocessors.

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 


Video