Tuesday, February 27, 2007

Avoid Invoke(), prefer BeginInvoke()

This is another post in a series that deals with asynchronous behaviour (see also here and here).

The difference between Control.Invoke() and Control.BeginInvoke() is:

  • BeginInvoke() will schedule the asynchronous action on the GUI thread. When the asynchronous action is scheduled, your code continues. Some time later (you don't know exactly when) your asynchronous action will be executed.
  • Invoke() will execute your asynchronous action (on the GUI thread) and wait until your action has completed.

A logical conclusion is that a delegate you pass to Invoke() can have out-parameters or a return-value, while a delegate you pass to BeginInvoke cannot (you have to use EndInvoke to retrieve the results).

My advice is: always use BeginInvoke() if you can. Reasons:

  • Because Invoke() causes one thread to wait for another thread, it is likely to cause deadlocks. Imagine that your application has a dedicated worker thread that uses Invoke() to execute some work on the GUI thread. Now image that during this work, your GUI code needs to execute something on the worker-thread. It will be blocked forever because your worker-thread is already busy waiting for your GUI-thread.
    In this simple example it might be easy to spot the problem. In a big application developed by many people it will not always be this easy. Therefore it is better to find a pattern that avoids blocking altogether.

    Note that in this example you might think that a threadpool solves your problem, because then you have multiple workerthreads. However the problem will still occur under high load, this is called threadpool starvation and it is a very complex problem. Because it only occurs under high load, it is a very dangerous thing that is very difficult to debug.
  • Invoke() will cause forced threadswitches that will decrease the scalability of your application.

    First consider BeginInvoke: when it is called, a delegate is scheduled to be executed on the other thread. However your current thread will continue until the operating system decides that it's done enough work. Only then will the other thread (GUI thread) be executed, and your delegate will be run. If other delegates were also scheduled using BeginInvoke(), they will be executed as well.

    Now consider Invoke: when you call it, the OS immedeately has to stop your current thread and schedule the other thread.
    When more than one Invoke()-statement is done, a threadswitch will be needed every single time (whereas multiple BeginInvoke-requests can be bundled in one threadswitch).
    Excessive threadswitches will seriously hurt performance when your application is under a high load.

Conclusion: avoid using Invoke().

20 comments:

Patrick Kursawe said...

Thank you very much - I immediately ran into a deadlock when trying to use Invoke(), nice to see that there's an easy way out.

Zytan said...

I deadlocked with Control.Invoke and Thread.Join, so it can easily happen, since Thread.Join is shown in the example code for threading.

Tim said...

Instead of using a custom Invoker delegate you could also use the System.Windows.Forms.MethodInvoker delegate...

The 'pattern' would be:

private void DoX(object input)
{
if (this.InvokeRequired)
{
this.EndInvoke(this.BeginInvoke(new MethodInvoker(delegate { this.DoX(input); })));
}
else
{
// perform actual work
}
}

Kristof said...

tim:
You're doing something strange here:
this.EndInvoke(this.BeginInvoke(...))

Immedeately calling EndInvoke() on the result you get from BeginInvoke() is actually just the same as calling Invoke() in the first place! And it has the same downsides that I discussed in this post.

henryv said...

I ran into the same issues when upgrading vs2003 to vs2005. Fortunelty I had a collegue who had experience in delgate use to showed me what was needed to do. Even after seeing these example

It was just so simple after that. Haven't incountered any issues yet with the Invoke() command over the program however given each thread only updates a particular cell in the listveiw hence no thread is really fighting for the same real estate, may have something to do with it. Anyway here is my solution seems very easy now.

clsthread.cs

// All the code is in the thread.class no need to putanything in the main form

delegate void Updatelistview1Invoker(ListView listview1, int row, int column, string message);

private void MainThread_Updatelistview1(ListView listview1, int row, int column, string message)
{
listview1.Items[row].SubItems[column].Text = message;
}

public void Updatelistview1(ListView listview1, int row, int column, string message)
{
listview1.Invoke(new Updatelistview1Invoker(MainThread_Updatelistview1), new object[] { listview1,row,column,message});
}

// Call this

Updatelistview1(listview1, row, column, "Abort");

mono said...

Hi,

Thanks for the great article. It helped me solved my problem.

Cheers,

Mono

Anonymous said...

thanks, helped me a lot.

Sushi said...

namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
PropertyPage pp = new PropertyPage();
public Form1()
{
InitializeComponent();

}
private void Form1_Load(object sender, EventArgs e)
{
MessageBox.Show("Serial Communication Test\n Sushant Khurana");
this.FormClosing += new FormClosingEventHandler(Form1_FormClosing);
Object[] Para = new Object[2];
Para[0] = sender;
Para[1] = e;
try
{
this.BeginInvoke(new SerialDataReceivedEventHandler(sp_DataReceived),new Object[]{Para});
}
catch (Exception er)
{
MessageBox.Show(er.Message);
}
}
private void SStatus_Click(object sender, EventArgs e)
{
if (pp.bRate == "" && pp.sBits == "")
{
BRlabel.Text = "Baud Rate = " + sp.BaudRate.ToString();
DBlabel.Text = "Data Bits = " + sp.DataBits.ToString();
}
else
{
BRlabel.Text = "Baud Rate = " + pp.bRate;
DBlabel.Text = "Data Bits = " + pp.sBits;
}
Plabel.Text = "Parity = " + sp.Parity.ToString();
SBlabel.Text = "Stop Bits = " + sp.StopBits.ToString();
RTlabel.Text = "ReadTimeout = " + sp.ReadTimeout.ToString();
ComPrt.Text = "Port Name = " + sp.PortName.ToString();
if (Property.Visible == true)
Property.Hide();
SStatus.Hide();
StartCom.Show();
sp.ReceivedBytesThreshold = 3;
sp.ReadTimeout = SerialPort.InfiniteTimeout;

}

private void Property_Click(object sender, EventArgs e)
{
pp.ShowDialog();
//Property.Hide();

}

private void Send_Click(object sender, EventArgs e)
{
BRlabel.Text = "";
try
{
sp.WriteLine(textBox.Text);
textBox.Text = "";
BRlabel.Text = "Text Sent Sucessfully\n";
}
catch(System.Exception ex)
{
BRlabel.Text = ex.Message;
}
}



private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{

MessageBox.Show("Thankyou for using this Application!");
sp.Close();
}

private void StartCom_Click(object sender, EventArgs e)
{
//Reading Text
BRlabel.Text = "";
try
{
sp.DtrEnable = true;
sp.RtsEnable = true;
sp.Handshake = Handshake.None;
//textBox.Text = "";
sp.DataReceived += new SerialDataReceivedEventHandler(sp_DataReceived);
sp.PinChanged += new SerialPinChangedEventHandler(plugged_in);
sp.Open();
}
catch (Exception ex)
{
BRlabel.Text = ex.Message;
}
//Text Read Finishes
StartCom.Hide();
Send.Show();
textBox.Show();
DBlabel.Text="";
Plabel.Text="";
SBlabel.Text="";
RTlabel.Text = "";
ComPrt.Text = "";
//BRlabel.Text = "";

}
private void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
{

try
{
byte[] b = new byte[3];
for (int i = 0; i < 3; i++)
b[i] = (byte)sp.ReadByte();
string s = b.ToString();
MessageBox.Show("DATA RECIEVED: \n" + s);

}
catch
{
Console.WriteLine("UH HUH!");
sp.Close();
}
Console.ReadLine();
}
private void plugged_in(object sender, SerialPinChangedEventArgs ea)
{
MessageBox.Show(" Device powered ON!/ OFF!");
}


}

}
I'm gettin parameter count mismatch in main(Program.cs), in the line Application.Run(new Form1) which is removed when I comment the line BeginInvoke.

I'm new to C#.NET, please help.

Kristof said...

@Sushi: I don't have the time to debug your code.

But at first sight, I find it a bit suspicious that you store your parameters in an object[], which is then stored in another object[].
Try changing your code to this:

this.BeginInvoke(new SerialDataReceivedEventHandler(sp_DataReceived),new Object[]{sender, e});

Note that you don't need the Para-object anymore.

Sushi said...

To an extent it did solve my problem, but the thing I've messed up is tht e is supposed to be of type EventArgs whereas I want an object of type SerialDataEventArgs.

BTW.. Thankx a lot for helping..

U ROck!!

Sushi said...

Is there a way.. (any way) I can check if the hardware is sucessfully connected to my computer through the serial Port

redware said...

Just to make it clear.... if you are having problems with threads on a control - don't use CheckForIllegalCrossThreadCalls as I have been doing but call the form method with BeginInvoke. I think you cannot pass parameters so set them up in form variables. Example below....

public void ShowMessage(string message)
{
// Use Begin invoke to avoid threading issues
_showmessagebegininvoke = message;
System.Threading.ThreadStart ts = ShowMessageBeginInvoke;
BeginInvoke(ts, null);
}

private void ShowMessageBeginInvoke()
{
//Control.CheckForIllegalCrossThreadCalls = false;
string message = _showmessagebegininvoke;
if (message == null) return;

if (this.txtmessage.Text.Length == 0)
{
this.txtmessage.Text = message;
}
else
{
this.txtmessage.Text = this.txtmessage.Text + Environment.NewLine + message;
}
_showmessagebegininvoke = null;
//Control.CheckForIllegalCrossThreadCalls = true;

}

Stamati said...

Just to make it clear - don't use
CheckForIllegalCrossThreadCalls as I have been doing but call your method on the form with begininvoke. I think you cannot pass parameters so set them in the form variables. Example below.

public void ShowMessage(string message)
{
// Use Begin invoke to avoid threading issues
_showmessagebegininvoke = message;
System.Threading.ThreadStart ts = ShowMessageBeginInvoke;
BeginInvoke(ts, null);
}

private void ShowMessageBeginInvoke()
{
//Control.CheckForIllegalCrossThreadCalls = false;
string message = _showmessagebegininvoke;
if (message == null) return;

if (this.txtmessage.Text.Length == 0)
{
this.txtmessage.Text = message;
}
else
{
this.txtmessage.Text = this.txtmessage.Text + Environment.NewLine + message;
}
_showmessagebegininvoke = null;
//Control.CheckForIllegalCrossThreadCalls = true;

}

ear said...

There will be a bit of inconsistancy in the behavior of methods too. If InvokeRequired is true it will have Async behavior else it'll be sync behavior!!! This could potentially cause problems.

ear said...

Hi, I forgot to checkmark followup to my last comment. Please followup to it! thanks!

Kristof said...

@ear: the InvokeRequired-pattern is mostly used for simple GUI updates. It should not matter if this is synchronous or asynchronous. But if it does matter, then you might choose a mechanism that is more consist (e.g. always using BeginInvoke even if it is not entirely necessary).

ear said...

what if one wants synchronous behavoir? how to get this from BeginInvoke() ? Was thinking about System.Threading.ManualResetEvent but would prefer something simpler. any ideas?

Kristof said...

@ear: if you really want synchronous behaviour, then you'll have to use Invoke(). You could use BeginInvoke() in combination with a ManualResetEvent, but that is probably what Invoke() is doing behind the scenes as well.
But generally speaking, Invoke/BeginInvoke are mostly used to update the GUI. And why would you need to do this synchronously?

Anonymous said...

One reason why asynchronous might not be appropriate for updating UI is if the user clicks a button in a quick, repetitive fashion. In this case if you use BeginInvoke and the code you're invoking (my case was loading a document into a 3rd party view control) this might take too long and cause the requests to pile up. When the user finally paused their clicking so that the application could catch up the result was that the remaining scheduled beginInvokes executed in 'LastInLastOut' fashion. So they actually were looking a document that was from several clicks ago.

Though, this 3rd party component was COM so perhaps this behavior was due to the extra layer.

Brandon Roberts said...

This article helped me so much! I was running into the same deadlock issue you had described and switching over to BeginInvoke cleared the issue right up! Thank you so much!