Friday, January 30, 2009

Parallel Programming in .Net 4.0 and VS2010: Part II – WinForms, Tasks and Service Level Agreements

Copyright 2008-2009, Paul Jackson, all rights reserved

Update 6/3/2010 – This original article was written using the Visual Studio 2010 CTP.  I’ve since updated the information in a new article which takes into account changes in the API as of the release of the VS2010 release.

The Console application in my previous post was actually the prototype for a more robust WinForms implementation – I have a customer who likes Math functions.  We’ll call him Bob.

image

Sure, Bob’s a little weird, but he typically pays his invoices Net-10, so I like to keep him happy.

I first deployed the application to Bob without using the Parallel Extensions, so it was single-threaded:

        private void goButton_Click(object sender, EventArgs e)
        {
            Stopwatch watch = Stopwatch.StartNew();
            for (int i = 0; i < 100; i++)
            {
                doWork(i);
            }
            watch.Stop();
            listBox1.Items.Add(String.Format("Entire process took {0} milliseconds", watch.ElapsedMilliseconds));
        }
        private void doWork(int instance)
        {
            Stopwatch watch = Stopwatch.StartNew();
            double result = Math.Acos(new Random().NextDouble()) * Math.Atan2(new Random().NextDouble(), new Random().NextDouble());
            for (int i = 0; i < 20000; i++)
            {
                result += (Math.Cos(new Random().NextDouble()) * Math.Acos(new Random().NextDouble()));
            }
            
            watch.Stop();
            listBox1.Items.Add(String.Format("{0} took {1} milliseconds", instance, watch.ElapsedMilliseconds));
        }

Bob was happy with the results, but not with the performance:

“Twelve seconds is too long!  I can’t wait that long!  Time is money in my business!  It needs to be instantaneous!”

I tried to explain to Bob that “instantaneous” is not a Service-Level Agreement, but he was adamant: “Faster!”

And, no, I have no idea what Bob’s business is or why he needs this application.  He pays on time – I don’t ask a lot of questions.

So my first change is to make the for-loop parallel:

            //for (int i = 0; i < 100; i++)
            Parallel.For(0, 100, i =>
                {
                    doWork(i);
                }
            );

Unfortunately, changing an application from single- to multi-threaded can have unintended consequences.  When writing a single-threaded WinForms application you don’t have to worry about things like WinForms Controls only being accessible from the UI thread:

image

The doWork() method accesses the ListBox directly to add items to it, but since doWork() is now being run on a different thread, it violates a fundamental Windows requirement that UI controls only be accessed from the thread they were created on.

This problem isn’t new with .Net 4.0, it’s always been there and remains even in WPF.  UI Controls can only be accessed from the thread that created them and that should be the main application thread.  So there are some hoops we have to jump through in order to get back to the UI thread in order to update the control.  There are a number of articles available that describe patterns for dealing with this – the one I typically use is:

        #region UI Threading Pattern
        private  delegate void addToListDelegate(string item);
        private void addToList(string item)
        {
            if (listBox1.InvokeRequired)
            {
                listBox1.BeginInvoke(new addToListDelegate(innerAddToList), new object[] { item });
            }
            else
            {
                innerAddToList(item);
            }
        }
        private void innerAddToList(string item)
        {
            listBox1.Items.Add(item);
        }
        #endregion

Then I change the doWork() method to add items to the list through the addToList() method instead of directly:

        private void doWork(int instance)
        {
            Stopwatch watch = Stopwatch.StartNew();
            double result = Math.Acos(new Random().NextDouble()) * Math.Atan2(new Random().NextDouble(), new Random().NextDouble());
            for (int i = 0; i < 20000; i++)
            {
                result += (Math.Cos(new Random().NextDouble()) * Math.Acos(new Random().NextDouble()));
            }
            
            watch.Stop();
            addToList(String.Format("{0} took {1} milliseconds", instance, watch.ElapsedMilliseconds));
        }

And, for consistency, I do the same to the goButton_Click where the total elapsed time is recorded.

Running the application now results in the same performance improvement seen in the console application:

image

I take this new version to Bob, pretty confident that dropping the processing time from 12.4 seconds to 3.8 will make him happy.  The only problem is that I’m not sure how to bill him for it – after all, using the Parallel Extensions I was able to get this speed improvement with only a few minutes of work -- .Net 4.0 might improve my productivity, but it could have a negative effect on my Accounts Receivable.

Unfortunately, Bob’s not as impressed as I thought he’d be:

“3.8 seconds?  I still have to wait 3.8 seconds?  Faster! Faster! Faster!  I need instantaneous results!  I need to start working with the list as soon as I click the Go button!”

Bob’s a little high-strung.

But something he said sparks an idea.  Bob needs to work with the list immediately, but not with the entire list.  His process is to do something with each item in the list (no, I still don’t know what he does with them), so he doesn’t need everything, he just needs enough to start working.  While he’s working on the early results, later results can be completed and returned.

The way we’d do that today is to start our own background thread using something like ThreadPool.QueueUserWorkItem().  I can move the current code from button click event to another method, then use QueueUserWorkItem to run that code on a background thread:

        private void goButton_Click(object sender, EventArgs e)
        {
            ThreadPool.QueueUserWorkItem(new WaitCallback(freeUi));
        }
        private void freeUi(object state)
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            Parallel.For(0, 100, (i) =>
                {
                    doWork(i);
                }
            );
            watch.Stop();
            addToList(String.Format("Entire process took {0} milliseconds", watch.ElapsedMilliseconds));
        }

This works great.  The list starts populating immediately and Bob will be able to get started working on the first items in the list while the remaining work finishes.  But QueueUserWorkItem is a little passé – it’s very … .Net 2.0 – what does 4.0 offer us to replace it with?

Enter the System.Threading.Tasks namespace and the Task class.  Using the StartNew() method on Task, we can create our background thread using the new, .Net 4.0 Task model, rather than via the old ThreadPool.  New is better.

Note: StartNew() was introduced in the September CTP of the Parallel Extensions, which is only available as part of the VS2010 CTP.  Using the June CTP in VS2008, you will have to Task.Create() a task and then Start() it.

        private void goButton_Click(object sender, EventArgs e)
        {
            //ThreadPool.QueueUserWorkItem(new WaitCallback(freeUi));
            System.Threading.Tasks.Task.StartNew(delegate { freeUi(null); });
        }

Bob is thrilled with this new version.

“I’m thrilled!  This is great!  It’s instantaneous! By the way … how do I make it stop?”

So Bob wants a way to stop the population of the list once he’s started it.  I take a quick look in the System.Threading.Tasks namespace, and, sure enough, StartNew() returns a reference to the new Task and the Task has a Cancel() method, so I can give Bob a Cancel button.

        private Task _task;
        private void goButton_Click(object sender, EventArgs e)
        {
            _task = Task.StartNew(delegate { freeUi(null); });
        }
        
        private void cancelButton_Click(object sender, EventArgs e)
        {
            if (_task != null)
                _task.Cancel();
        }

There’s one more step that I need to take, and that’s to also cancel the Parallel.For loop in the freeUi() method.  Each time through that loop is a separate Task (that has the Task running freeUi() as its parent), so as part of that loop I need to check to see if the parent task has been canceled.  I also want cancel any remaining iterations of that loop, so I’ll use a different delegate that brings in a ParallelState object – among other things, this class has a Stop() method that will stop execution of the loop.

Note: As of this writing, the cancellation method for a Task and its children, like everything about this prerelease, is subject to change prior to the release of .Net 4.0.  This is a scenario that will likely be improved before final release.

        private void freeUi(object state)
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            Task _currentTask = Task.Current;
            Parallel.For(0, 100, (i, loopState) =>
                {
                    if (_currentTask.IsCancellationRequested)
                    {
                        loopState.Stop();
                        return;
                    }
                    doWork(i);
                }
            );
            if (_currentTask.IsCancellationRequested)
            {
                addToList("Process cancelled");
                return;
            }
            watch.Stop();
            addToList(String.Format("Entire process took {0} milliseconds", watch.ElapsedMilliseconds));
        }

Bob’s happy with this latest version.  It satisfies both reasons to parallelize or multi-thread an application: Performance and User Experience.  Bob’s experience is improved because he’s able to continue work immediately after clicking on the Go button and doesn’t have to wait at all, and the entire process’ execution time has improved from over twelve seconds to under four (on a quadcore PC).  Even I’m happy, because I can now send Bob an invoice and get paid.

Download the source code:

3 comments:

BalaMurugan J said...

Dear Paul Jackson,
Great article. I am really happy with this article. It works well in windows applications (Win forms).I tried this logic in my web applications but it throws "Cross thread Operation not valid exception". You told some solution in this article.
if (listBox1.InvokeRequired)
{
listBox1.BeginInvoke(new addToListDelegate(innerAddToList), new object[] { item });
}
I tried this for web application, I can’t get this (InvokeRequired) property and BeginInvoke() method. Is this logic(InvokeRequired,BeginInvoke()) only for Win forms? How to achieve parallel programming for web applications? Please give some idea.

Paul Jackson said...

Bala,

Web applications and controls are a little different. If you think about it, they're already inherently multi-threaded, with each user's request being executed on a seperate thread. So there tends not to be a benefit in parallelizing the request, because with a number of users you might overload the system with threads. Does that make sense?

farSWan said...

Brilliant Article