none
BackgroundWorker performance RRS feed

  • Question

  • Is it normal that DoWork callback functions are faster the more often they call ReportProgess()?

    For example, the following code is significantly faster then the second one.

    BackgroundWorker bg = sender as BackgroundWorker;
    
    int i = 0;
    foreach(...)
    {
      foreach(...)
      {
        (...)
        bg.ReportProgress(i);
        i++;
      }
    }
    BackgroundWorker bg = sender as BackgroundWorker;
    
    int i = 0;
    foreach(...)
    {
      foreach(...)
      {
        (...)
        i++;
      }
    }
    bg.ReportProgress(i);



    • Moved by CoolDadTx Monday, January 11, 2016 4:02 PM Office related
    Monday, January 11, 2016 11:03 AM

Answers

  • Your way to declare the Display elements in the form is odd. Normally that kind of code is left/autodone in the Form.designer.cs file. You also do not call InitialiseComponents in the Constructor. But that might be either because you work without a Forms designer or for example purposes or because this is Addin Programming.

    One issue might be that you hand in a very complex type (Microsoft.Office.Interop.Word.Document) into the BGW. This may cause issues because now two threads have access to the document (original GUI thread and wherever the Background Thread inside the BGW).
    There is no mention of thread-safety for this interface or it's potential implementations (we are not even sure how that class looks at runtime).
    Personally I make it a habit to only hand string or primitive types in or out of a BGW. Nothing can go wrong with those two. And if you can't do it with either of those, it might not be doable at all.

    You are also using the old .COM based Office Word Interop. .COM does not work so well with Multithreading (among other isses). No idea if the interop classes account for that and if so how they do that (create a new instance? Locking to avoid sinchronisation issues? If so, when is the other threads lock released?).

    This is also MS Word Addin programming. All sorts of issues can come from that front. Like Word changing how and when the BGW thread is executed (putting it at low priority in a threadpool shared by stuff like the Spellcheck).

    This may shed some light on the last two points:
    https://msdn.microsoft.com/en-us/library/8sesy69e.aspx
    • Edited by Christopher84 Monday, January 11, 2016 3:50 PM
    • Marked as answer by Lugaxx Monday, January 11, 2016 4:54 PM
    Monday, January 11, 2016 3:48 PM

All replies

  • Is it normal that DoWork callback functions are faster the more often they call ReportProgess()?

    For example, the following code is significantly faster then the second one.

    BackgroundWorker bg = sender as BackgroundWorker;
    
    int i = 0;
    foreach(...)
    {
      foreach(...)
      {
        (...)
        bg.ReportProgress(i);
        i++;
      }
    }
    BackgroundWorker bg = sender as BackgroundWorker;
    
    int i = 0;
    foreach(...)
    {
      foreach(...)
      {
        (...)
        i++;
      }
    }
    bg.ReportProgress(i);



    Monday, January 11, 2016 10:22 AM
  • BackgroundWorker and VSTO are entirely different things not related to each other. I'd suggest asking commong programming questions on the Visual C# forum thread.
    • Marked as answer by Lugaxx Monday, January 11, 2016 11:03 AM
    Monday, January 11, 2016 10:40 AM
  • No, calling Report Progress often should not positively afect performance.
    All it does is tax the GUI thread with many and (possibly) costly writes, eventually even negating the usefullness of a BGW altogether (because the UI is still locked up).
    It might lower the cost of the final RunWorkerCompleted event accidentally, as it makes the writing needed then a lot less.

    Progress Changed and RunWorker Completed events are both automagically invoked on the GUI thread. As such they have all the weaknesses any other GUI event has, for taxing out the GUI thread.
    So whatever you observe might be an effect of the GUI write overhead and has nothng to do with a BGW. All a BGW does is encapsulate a thread, give you some error relaying and doing invoking with the 2 events that need it (all three very usefull for beginners).

    GUI write overhead example:

    using System;
    using System.Windows.Forms;
    
    namespace UIWriteOverhead
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            int[] getNumbers(int upperLimit)
            {
                int[] ReturnValue = new int[upperLimit];
    
                for (int i = 0; i < ReturnValue.Length; i++)
                    ReturnValue[i] = i;
    
                return ReturnValue;
            }
    
            void printWithBuffer(int[] Values)
            {
                textBox1.Text = "";
                string buffer = "";
    
                foreach (int Number in Values)
                    buffer += Number.ToString() + Environment.NewLine;
                textBox1.Text = buffer;
            }
    
            void printDirectly(int[] Values){
                textBox1.Text = "";
    
                foreach (int Number in Values)
                    textBox1.Text += Number.ToString() + Environment.NewLine;
            }
    
            private void btnPrintBuffer_Click(object sender, EventArgs e)
            {
                MessageBox.Show("Generating Numbers");
                int[] temp = getNumbers(10000);
                MessageBox.Show("Printing with buffer");
                printWithBuffer(temp);
                MessageBox.Show("Printing done");
            }
    
            private void btnPrintDirect_Click(object sender, EventArgs e)
            {
                MessageBox.Show("Generating Numbers");
                int[] temp = getNumbers(1000);
                MessageBox.Show("Printing directly");
                printDirectly(temp);
                MessageBox.Show("Printing done");
            }
        }
    }
    

    Here is my often used example code on how to use a BGW:

    #region Primenumbers
    private void btnPrimStart_Click(object sender, EventArgs e)
    {
    	if (!bgwPrim.IsBusy)
    	{
    		//Prepare ProgressBar and Textbox
    		int temp = (int)nudPrim.Value;
    		pgbPrim.Maximum = temp;
    		tbPrim.Text = "";
    
    		//Start processing
    		bgwPrim.RunWorkerAsync(temp);
    	}
    }
    
    private void btnPrimCancel_Click(object sender, EventArgs e)
    {
    	if (bgwPrim.IsBusy)
    	{
    		bgwPrim.CancelAsync();
    	}
    }
    
    private void bgwPrim_DoWork(object sender, DoWorkEventArgs e)
    {
    	int highestToCheck = (int)e.Argument;
    	//Get a reference to the BackgroundWorker running this code
    	//for Progress Updates and Cancelation checking
    	BackgroundWorker thisWorker = (BackgroundWorker)sender;
    
    	//Create the list that stores the results and is returned by DoWork
    	List<int> Primes = new List<int>();
    	
    
    	//Check all uneven numbers between 1 and whatever the user choose as upper limit
    	for(int PrimeCandidate=1; PrimeCandidate < highestToCheck; PrimeCandidate+=2)
    	{
    		//Report progress
    		thisWorker.ReportProgress(PrimeCandidate);
    		bool isNoPrime = false;
    
    		//Check if the Cancelation was requested during the last loop
    		if (thisWorker.CancellationPending)
    		{
    			//Tell the Backgroundworker you are canceling and exit the for-loop
    			e.Cancel = true;
    			break;
    		}
    
    		//Determin if this is a Prime Number
    		for (int j = 3; j < PrimeCandidate && !isNoPrime; j += 2)
    		{
    			if (PrimeCandidate % j == 0)
    				isNoPrime = true;
    		}
    
    		if (!isNoPrime)
    			Primes.Add(PrimeCandidate);
    	}
    
    	//Tell the progress bar you are finished
    	thisWorker.ReportProgress(highestToCheck);
    
    	//Save Return Value
    	e.Result = Primes.ToArray();
    }
    
    private void bgwPrim_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
    	pgbPrim.Value = e.ProgressPercentage;
    }
    
    private void bgwPrim_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
    	pgbPrim.Value = pgbPrim.Maximum;
    	this.Refresh();
    
    	if (!e.Cancelled && e.Error == null)
    	{
    		//Show the Result
    		int[] Primes = (int[])e.Result;
    
    		StringBuilder sbOutput = new StringBuilder();
    
    		foreach (int Prim in Primes)
    		{
    			sbOutput.Append(Prim.ToString() + Environment.NewLine);
    		}
    
    		tbPrim.Text = sbOutput.ToString();
    	}
    	else 
    	{
    		tbPrim.Text = "Operation canceled by user or Exception";
    	}
    }
    #endregion
    
    Monday, January 11, 2016 11:19 AM
  • I'm working on a VSTO add-in for Word. And I've noticed that my computation takes more than 5x times longer when using a background worker (3s vs 17s). Are there any alternatives to BackgroundWorker? All I want is to display a progress bar while the calculation is in progress. I can't believe that this is so expensive. What about a simple "Please wait..." message box? How expensive would that be?
    Monday, January 11, 2016 12:48 PM
  • I'm working on a VSTO add-in for Word. And I've noticed that my computation takes more than 5x times longer when using a background worker (3s vs 17s).

    It is mathematically impossible to be an issue with the BGW. All that one does is do the same work in an alternate thread. If it takes 3 seconds in thread A, it should take 3 seconds in thread B. The only "runtime overhead" work is does, is have a try...catch block around the call to the RunWorker event. And that can't possibly account for a x6 running cost.

    Please show us your actuall code. In particular the loop, progress report and run worker completed events are the most likely sources of slowdown.

    Monday, January 11, 2016 12:54 PM
  • Here is my form:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    using Word = Microsoft.Office.Interop.Word;
    
    namespace Test
    {
        public partial class Form1 : Form
        {
            public Label statusLabel;
            public ProgressBar progressBar;
            public Button cancelButton;
    
            public BackgroundWorker backgroundWorker;
    
            public ThisAddIn thisAddIn;
    
            public Form1()
            {
                this.statusLabel = new Label()
                {
                    Dock = DockStyle.Top,
                    Text = "Bitte warten...",
                };
                this.progressBar = new ProgressBar()
                {
                    Dock = DockStyle.Fill,
                    Style = ProgressBarStyle.Marquee,
                };
                this.cancelButton = new Button()
                {
                    Dock = DockStyle.Bottom,
                    Text = "Abbrechen",
                };
                this.backgroundWorker = new BackgroundWorker()
                {
                    WorkerReportsProgress = true,
                    WorkerSupportsCancellation = true,
                };
    
                this.backgroundWorker.DoWork += worker_DoWork;
                this.backgroundWorker.ProgressChanged += worker_ProgressChanged;
                this.backgroundWorker.RunWorkerCompleted += worker_RunWorkerCompleted;
    
                this.cancelButton.Click += cancelButton_Click;
    
                this.SuspendLayout();
                this.AutoSize = true;
                this.Controls.Add(this.statusLabel);
                this.Controls.Add(this.progressBar);
                this.Controls.Add(this.cancelButton);
                this.ResumeLayout(false);
                this.PerformLayout();
            }
    
            void worker_DoWork(object sender, DoWorkEventArgs e)
            {
                var watch = System.Diagnostics.Stopwatch.StartNew();
    
                this.thisAddIn.Application.Visible = false;
    
                Word.Document document = e.Argument as Word.Document;
                
                BackgroundWorker backgroundWorker = sender as BackgroundWorker;
    
                int i = 0;
                foreach (Word.Table table in document.Tables)
                {
                    foreach (Word.Paragraph paragraph in table.Range.Paragraphs)
                    {
                        Word.Style style = paragraph.get_Style() as Word.Style;
                        if (style.NameLocal == document.Styles[Word.WdBuiltinStyle.wdStyleHeading1].NameLocal)
                        {
                            backgroundWorker.ReportProgress(i);
    
                            if (backgroundWorker.CancellationPending)
                            {
                                e.Cancel = true;
                                break;
                            }
    
                            Word.ContentControl contentControl = document.ContentControls.Add(Word.WdContentControlType.wdContentControlRichText, paragraph);
                            contentControl.LockContentControl = true;
                            contentControl.Appearance = Word.WdContentControlAppearance.wdContentControlTags;
                            contentControl.Title = "";
                            contentControl.Tag = "#" + i;
                            contentControl.Color = Word.WdColor.wdColorRed;
                            contentControl.LockContentControl = false;
                            contentControl.Temporary = true;
    
                            i++;
                        }
                    }
                }
    
                backgroundWorker.ReportProgress(i);
    
                this.thisAddIn.Application.Visible = true;
    
                watch.Stop();
                System.Diagnostics.Debug.WriteLine("ms = " + watch.ElapsedMilliseconds);
            }
    
            void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
            {
                //statusLabel.Text = "" + e.UserState as String;
                statusLabel.Text = e.ProgressPercentage.ToString();
            }
    
            void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
            {
                if (e.Cancelled)
                {
                    statusLabel.Text = "Berechnung abgebrochen!";
                }
                else if (e.Error != null)
                {
                    statusLabel.Text = "Exception!";
                }
                else
                {
                    statusLabel.Text = "Berechnung abgeschlossen!";
                }
                
            }
    
            private void cancelButton_Click(object sender, EventArgs e)
            {
                if (backgroundWorker.IsBusy)
                {
                    backgroundWorker.CancelAsync();
                }
            }
        }
    }
    

    And this is how I start the form / background worker:

    Form1 f = new Form1()
    {
      thisAddIn = this.thisAddIn;
    };
    f.Show();
    f.backgroundWorker.RunWorkerAsync(document);

    Monday, January 11, 2016 1:21 PM
  • Use Stopwatch to time the entire body of your DoWork method and you'll find that it is probably slower. More likely what you are seeing is the illusion of the code running faster because your UI is being updated continually. It is one of the reasons why we like async stuff so much. It generally takes longer but feels faster.

    Michael Taylor
    http://blogs.msmvps.com/p3net

    Monday, January 11, 2016 3:09 PM
  • But this still doesn't explain why it only takes 3 seconds when I put the actual/productive code (my foreach loop) into the main thread. What's causing this overhead of 13s?

    Monday, January 11, 2016 3:35 PM
  • Hello Lugaxx,

    Ideally calling any function frequently will actually cause running more line of code and Its calling another thread also. 

    Ideally there should not be any reason for calling a UI update so frequently.

    Also one thing if you can do: please check for some greater value of your loop counter so that time can be checked with larger interval and actual issue can be in your environment ( not talking about VSTO or dev env onlly).


    A user friendly computer first requires a friendly user

    Monday, January 11, 2016 3:44 PM
  • Your way to declare the Display elements in the form is odd. Normally that kind of code is left/autodone in the Form.designer.cs file. You also do not call InitialiseComponents in the Constructor. But that might be either because you work without a Forms designer or for example purposes or because this is Addin Programming.

    One issue might be that you hand in a very complex type (Microsoft.Office.Interop.Word.Document) into the BGW. This may cause issues because now two threads have access to the document (original GUI thread and wherever the Background Thread inside the BGW).
    There is no mention of thread-safety for this interface or it's potential implementations (we are not even sure how that class looks at runtime).
    Personally I make it a habit to only hand string or primitive types in or out of a BGW. Nothing can go wrong with those two. And if you can't do it with either of those, it might not be doable at all.

    You are also using the old .COM based Office Word Interop. .COM does not work so well with Multithreading (among other isses). No idea if the interop classes account for that and if so how they do that (create a new instance? Locking to avoid sinchronisation issues? If so, when is the other threads lock released?).

    This is also MS Word Addin programming. All sorts of issues can come from that front. Like Word changing how and when the BGW thread is executed (putting it at low priority in a threadpool shared by stuff like the Spellcheck).

    This may shed some light on the last two points:
    https://msdn.microsoft.com/en-us/library/8sesy69e.aspx
    • Edited by Christopher84 Monday, January 11, 2016 3:50 PM
    • Marked as answer by Lugaxx Monday, January 11, 2016 4:54 PM
    Monday, January 11, 2016 3:48 PM
  • "...(my foreach loop) into the main thread"

    I don't see any code that you posted where your foreach loop is running on the main thread. The foreach loop is always running in a background thread because it is inside your DoWork handler.

    "What's causing this overhead of 13s?"

    Background work can cause that delay. Calling ReportProgress frequently will slow things down because each call requires that the worker thread be paused, wait for the UI thread to be available, run your progress handler and then resume the worker thread. The more frequently you call it the slower your code will get.

    But you have a serious issue here.  Office objects are not thread safe. You are accessing Office on a worker thread. If it works at all you're lucky.  This question really needs to go over to the Office forums where they can better help you with VSTO as, to me, this is an unsupported scenario.

    Monday, January 11, 2016 4:02 PM
  • You are also using the old .COM based Office Word Interop. .COM does not work so well with Multithreading (among other isses). No idea if the interop classes account for that and if so how they do that (create a new instance? Locking to avoid sinchronisation issues? If so, when is the other threads lock released?)

    Where do I find information one the new Office Word Interop?
    Monday, January 11, 2016 4:07 PM
  • I don't see any code that you posted where your foreach loop is running on the main thread. The foreach loop is always running in a background thread because it is inside your DoWork handler.

    If I use in my main tread the same foreach loop which I use in the background worker then it takes only 3s.

    Background work can cause that delay. Calling ReportProgress frequently will slow things down because each call requires that the worker thread be paused, wait for the UI thread to be available, run your progress handler and then resume the worker thread. The more frequently you call it the slower your code will get.

    Yes, I understand that. But there must be an explanation for this. Again, if I do not use ReportProgress at all than the execution takes 23s. If I use ReportProgress in every iteration it takes 17s. And if I do not use the background worker at all it takes 3s.

    No background worker: 3s

    Background worker / ReportProgress every iteration: 17s

    Background worker / no ReportProgress: 23s

    But you have a serious issue here.  Office objects are not thread safe. You are accessing Office on a worker thread. If it works at all you're lucky.  This question really needs to go over to the Office forums where they can better help you with VSTO as, to me, this is an unsupported scenario.

    Okay, thanks.


    • Edited by Lugaxx Monday, January 11, 2016 4:20 PM
    Monday, January 11, 2016 4:19 PM
  • If you have to support the old formats (.doc, .xls) you can not get around the COM library. We always had that, so there was never any real need to develop something else.

    If you can limit it to the new office formats (.???x), you can use the Open XML SKD. Or ZipArchive and XML reader classes. These formats a bunch of XML files in a renamed zip container, with well known public definition.
    They are nothing like the old, propeitary ones.
    However I have no idea if they work at all/predictable from a Office Addin context.

    The only real solution might be to continue using the COM library, but create the instances inside the thread and use the proper Thread settings.
    This thread was already mooved to the proper Subforum, but you might be better of asking how to do Multithreading from Addins at all.

    Monday, January 11, 2016 11:12 PM