Real Multi-threading in .NET 2.0

Sample Image - RealMultiThreading.jpg

Introduction

Multithreading has always impressed me. Being able to do lots of things at once is really impressive, but we can’t do this if we don’t have the proper hardware. Till now, all we could do is separate the hard CPU work in a background thread and thus leave the user interface unblocked. I wanted to go further than this, exploiting the newest capabilities of new CPUs (at user’s hands) and try to get a real working multithreading example, that is, with more than one thread running in the background.

That is what this article is all about, and have to say, that the results have impressed me. Hope you will find it interesting. Well, in a multi cpu server with 4 CPU’s the benefits are of 280% (in a cpu-intensive job) and on normal machines with non-cpu intensive jobs it can go from 500% to 1000% in performance improvements…

Background

There are a lot of introductory articles to multithreading for .Net 2.0 and, have to say, they have helped me a lot. What I have used is the BackgroundWorker .Net 2.0 component (but there are code implementations for it on Net 1.1 that do the job.

Here I put some links to these articles:
  • Good introductory article from Paul Kimmel. [^]
  • Other introductory article, from Juval Löwy[^]
  • impressive site of Joseph Albahari dedicated almost to threading in c#... a must see…[^]
  • Safe, Even Simpler Multithreading in Windows Forms 2.0, from Michael Weinhardt, I took the cpu-intensive job from his web. Well, he took it from Chris Sell’s article (he refers him – as I do ^_^)[^]

    Well these are must read articles if you’re new to the threading world or if you’re not but want to get updated with the new Net 2.0’s BackgroundWorker component..

    Problems addressed

    Any kind of problem… being it a processor intensive or a normal task:

  • Processor intensive task : It can be divided into one, two or more threads which will be used by a CPU each (multiplying the performance by each processor..)
  • Normal task : Each “normal” task if done sequentially has a “delay” whenever it accesses the file system for reading or writing data, if it gets access to data storage or if it uses a web service… all of this means time that normally is unused and counts as a delay for the user response or the task itself. With a multiple threaded job management, there will be no delay. These time will be assigned and used by parallel tasks and used, not lost. That will result in, per example on a 100 package with 100ms of delay each the performance difference of a 1 thread model to a 20thread model was about 1000% in performance.

     

    Let’s say that if the problem is building the blocks of a web site page, instead of doing these sequentially , taking 1-4 seconds into having all the sections built; the banner, the users online, the last articles, the most voted tools, etc… what if we could build all of these asynchronously and when they’re built up send them to the user? We will save the webservice calls, the database calls and a lot of other precious time… and more on! These calls would be serviced faster and that would mean that the possibilities of coinciding the calls would be reduced, increasing the response times substantially. Interesting?

    The solution is already here..

    It is called BackgroundWorker and for our intentions we will subclass it. Background worker helps us to set-up a “Worker” for doing a work in an asynchronous way.
    What we want to do is set up a Factory (oops, no design patterns meaning here) where one kind of job will be done, that will mean thaw we will have a kind of job, some process, and some workers that know how to do this job.
    Of course, we will need a manager for assigning the jobs to the workers and what to do when they reach a step of the job and when they finish it. And yes, also we want the manager to be able to speak to the workers to stop. They have to take a rest too! And when the manager says so, they must stop!
    We will explain thins from bottom to top, beginning from the Worker and then we will see the Manager.

    The Worker

    It is a subclass of the Background worker, we set up the constructor to assign to true two properties of BackgroundWorker that are WorkerReportsProgress and WorkerSupportsCancellation which will enable us to do what the names say, report progress, normally to an UI and cancel the job (and subsequently all jobs) if they take too long. We also assign a id number to each worker. The manager needs to control them, though. Here’s the code:

       public class MTWorker : BackgroundWorker
    {
    #region Private members
    private int _idxLWorker = 0;
    #endregion
    #region Properties
    public int IdxLWorker
    {
    get { return _idxLWorker; }
    set { _idxLWorker = value; }
    }
    #endregion
    #region Constructor
    public MTWorker()
    {
    WorkerReportsProgress = true;
    WorkerSupportsCancellation = true;
    }
    public MTWorker(int idxWorker)
    : this()
    {
    _idxLWorker = idxWorker;
    }
    #endregion
    

    Also, we will override another of the BackgroundWorker’s methods. In fact the most interesting one, which does the real Job. And it means it. It’s name is OnDoWork and it is the method that is called when we invoke or launch the multithreaded task. Here we manage the start-up of the task, its progress, its cancellation and its completion. I have added two possible jobs to do, one “Normal” that emulates with delay’s the waiting time of a non cpu-inensive task with has to ask and wait for filesystem, network, database or webservices calls… and other which is a CPU intensive Job: Calculating the PI number. You can play with it and see the results of giving more or less delay and increasing the thread’s number (Oops, I meant the worker’s numbers…).
    Here is the OnDoWork code:

    Collapse
    protected override void OnDoWork(DoWorkEventArgs e)
    {
    //Here we receive the necessary data for doing the work...
       //we get an int but it could be a struct, class, whatever..
       int digits = (int)e.Argument;
    double tmpProgress = 0;
    int Progress = 0;
    String pi = "3";
    // This method will run on a thread other than the UI thread.
       // Be sure not to manipulate any Windows Forms controls created
       // on the UI thread from this method.
       this.ReportProgress(0, pi);
    //Here we tell the manager that we start the job..
    
    Boolean bJobFinished = false;
    int percentCompleteCalc = 0;
    String TypeOfProcess = "NORMAL"; //Change to "PI" for a cpu intensive task
       //Initialize calculations
       while (!bJobFinished)
    {
    if (TypeOfProcess == "NORMAL")
    {
    #region Normal Process simulation, putting a time
    delay to emulate a wait-for-something
    while (!bJobFinished)
    {
    if (CancellationPending)
    {
    e.Cancel = true;
    return; //break
                }
    //Perform another calculation step
                Thread.Sleep(250);
    percentCompleteCalc = percentCompleteCalc + 10;
    if (percentCompleteCalc >= 100)
    bJobFinished = true;
    else
    ReportProgress(percentCompleteCalc, pi);
    }
    #endregion
    }
    else
    {
    #region  Pi Calculation - CPU intensive job,
    beware of it if not using threading ;) !!
    //PI Calculation
             if (digits > 0)
    {
    pi += ".";
    for (int i = 0; i < digits; i += 9)
    {
    // Work out pi. Scientific bit :-)
                   int nineDigits = NineDigitsOfPi.StartingAt(i + 1);
    int digitCount = System.Math.Min(digits - i, 9);
    string ds = System.String.Format("{0:D9}", nineDigits);
    pi += ds.Substring(0, digitCount);
    // Show progress
                   tmpProgress = (i + digitCount);
    tmpProgress = (tmpProgress / digits);
    tmpProgress = tmpProgress * 100;
    Progress = Convert.ToInt32(tmpProgress);
    ReportProgress(Progress, pi);
    // Deal with possible cancellation
                   if (CancellationPending) //If the manager says to stop, do so..
                   {
    bJobFinished = true;
    e.Cancel = true;
    return;
    }
    }
    }
    bJobFinished = true;
    #endregion
    }
    }
    ReportProgress(100, pi); //Last job report to the manager ;)
       e.Result = pi; //Here we pass the final result of the Job
    }
    

    The Manager

    Here is what has more fun and I am pretty sure that it can be improved a lot – any comment or improvement are welcome! What it does is to generate and configure a Worker for each Thread and then it assigns the jobs to them. By now the only parameter it passes to the worker is a number, but it could pass a class or struct with all the job definition… A possible upgrade would be to implement a strategy pattern here for choosing how to do the internal job.
    Well we then call the InitManager which configures the jobs, its number, the specs of the jobs to do and then creates an array of MultiThread Workers and configures them. The configuration code follows.

    private void ConfigureWorker(MTWorker MTW)
    {
    //We associate the events of the worker
      MTW.ProgressChanged += MTWorker_ProgressChanged;
    MTW.RunWorkerCompleted += MTWorker_RunWorkerCompleted;
    }
    

    Like this, the Worker’s subclassed thread management Methods are linked to the Methods held by the Manager. Note that with a Strategy pattern implemented we could assign these to the proper manager for these methods.
    Then we have the most important method, the AssignWorkers. What it does is to check all the workers and if there is anyone that is not working it assigns a job to it. That is, if there are jobs left to process. When it finishes checking workers, if we found that there is no worker working (and thus we have not assigned any job too) that will mean the end of the jobs. No more to do, all thing’s done!
    Here’s the code:

    Collapse
    public void AssignWorkers()
    {
    Boolean ThereAreWorkersWorking = false;
    //We check all workers that are not doing a job... and assign a new one
       foreach (MTWorker W in _arrLWorker)
    {
    if (W.IsBusy == false)
    {
    //If there are still jobs to be done...
             //we assign the job to the free worker
             if (_iNumJobs > _LastSentThread)
    {
    //We control the threads associated to a worker
              //(not meaning the jobs done) just 4 control.
              _LastSentThread = _LastSentThread + 1;
    W.JobId = _LastSentThread; //We assign the job number..
              W.RunWorkerAsync(_iPiNumbers); //We pass the parameters for the job.
              ThereAreWorkersWorking = true;
    //We have at least this worker we just assigned the job working..
             }
    }
    else
    {
    ThereAreWorkersWorking = true;
    }
    }
    if (ThereAreWorkersWorking == false)
    {
    //This means that no worker is working and no job has been assigned.
          //this means that the full package of jobs has finished
          //We could do something here...
          Button BtnStart = (Button)FormManager.Controls["btnStart"];
    Button BtnCancel = (Button)FormManager.Controls["btnCancel"];
    BtnStart.Enabled = true;
    BtnCancel.Enabled = false;
    MessageBox.Show("Hi, I'm the manager to the boss (user): " +
    "All Jobs have finished, boss!!");
    }
    }

    We call this method whenever a job is finished. This way it ensures the completion of all jobs.
    We also link the form through a property of this class so we can associate it to any form we want. Well we could want to link it to another class, but this is the most normal thing to do.
    Well… improving it we could get a BackgroundManager for all our application needs..

    The UI

    Last but not less important, we link all this to a form. The code is minimal and it’s pretty simple: we add a reference to the Manager, we configure it on the form’s constructor and on a start button we call the Manager’s LaunchManagedProcess.

    private MTManager LM;
    public Form1()
    {
    InitializeComponent();
    LM = new MTManager(this, 25);
    LM.InitManager();
    }
    private void btnStart_Click(object sender, EventArgs e)
    {
    btnStart.Enabled = false;
    btnCancel.Enabled = true;
    LM.LaunchManagedProcess();
    }
    private void btnCancel_Click(object sender, EventArgs e)
    {
    LM.StopManagedProcess();
    btnCancel.Enabled = false;
    btnStart.Enabled = true;
    }
    

    Trying it!

    This is the funniest part, changing the properties of how many threads to run simultaneously and how many Jobs to be processed and then try it on different CPU’s… ah, and of course, change the calculation method from a CPU-intensive task to a normal task with a operation delay...
    I would love to know your results and what have you done with this, any feedback would be great!!

    Exercises For You…

    This is not done! It could be a MultiThreadJob Framework if there is being done the following:

  • Implement a Strategy pattern that determines the kind of Worker to produce (with a factory pattern) so we will be able to do different kind of jobs inside the same factory.. what about migrating a database and processing each table in a different way… or integrating systems with this engine…
  • Implement -or extend- the strategy pattern for determining the treatment for the Input data and the result data of the jobs. We could too set-up a factory for getting the classes into a operating environment.
  • Optimize the AssignWorkers engine – I am pretty sure it can be improved.
  • Improve the WorkerManager class in order to be able to attach it to another class instead to only a form.
  • Send me the code! I Would love to hear from you and what have you done.
  • License

    This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

    A list of licenses authors might use can be found here

    About the Author

    Jose Luis Latorre


    Professional developer since 1991, having developed on multiple systems and languages since then, from Unix, as400, lotus notes, flash, javascript, asp, prolog, vb, c++, vb.Net, C#...

    Now I'm focused on .Net development, both windows and web with two-three year experience on both and fully up-to date with 2.0 .Net in both Vb and C#
    Also have experience with SQL server 2005 and Business Intelligence.

    Jose Luis Lives in Barcelona, Spain, with his cat Pancho. To contact Jose Luis, email him at
    joslat@gmail.com.


    Occupation: Web Developer
    Location: Spain Spain
    posted @ 2008-09-08 13:21  Mose  阅读(325)  评论(0编辑  收藏  举报