Reputation: 4317
I know you can read a gui control from a worker thread without using Invoke/BeginInvoke because my app is doing it now. The cross thread exception error is not being thrown and my System.Timers.Timer thread is able to read gui control values just fine (unlike this guy: can a worker thread read a control in the GUI?)
Question 1: Given the cardinal rule of threads, should I be using Invoke/BeginInvoke to read form control values? And does this make it more thread-safe? The background to this question stems from a problem my app is having. It seems to randomly corrupt form controls another thread is referencing. (see question 2)
Question 2: I have a second thread that needs to update form control values so I Invoke/BeginInvoke to update those values. Well this same thread needs a reference to those controls so it can update them. It holds a list of these controls (say DataGridViewRow objects). Sometimes (not always), the DataGridViewRow reference gets "corrupt". What I mean by corrupt, is the reference is still valid, but some of the DataGridViewRow properties are null (ex: row.Cells). Is this caused by question 1 or can you give me any tips on why this might be happening?
Here's some code (the last line has the problem):
public partial class MyForm : Form
{
void Timer_Elapsed(object sender)
{
// we're on a new thread (this function gets called every few seconds)
UpdateUiHelper updateUiHelper = new UpdateUiHelper(this);
// Is it thread-safe to step through the datagrid rows here without invoking?
foreach (DataGridViewRow row in dataGridView1.Rows)
{
object[] values = GetValuesFromDb();
updateUiHelper.UpdateRowValues(row, values[0]);
}
// .. do other work here
updateUiHelper.UpdateUi();
}
}
public class UpdateUiHelper
{
private readonly Form _form;
private Dictionary<DataGridViewRow, object> _rows;
private delegate void RowDelegate(DataGridViewRow row);
private readonly object _lockObject = new object();
public UpdateUiHelper(Form form)
{
_form = form;
_rows = new Dictionary<DataGridViewRow, object>();
}
public void UpdateRowValues(DataGridViewRow row, object value)
{
if (_rows.ContainsKey(row))
_rows[row] = value;
else
{
lock (_lockObject)
{
_rows.Add(row, value);
}
}
}
public void UpdateUi()
{
foreach (DataGridViewRow row in _rows.Keys)
{
SetRowValueThreadSafe(row);
}
}
private void SetRowValueThreadSafe(DataGridViewRow row)
{
if (_form.InvokeRequired)
{
_form.Invoke(new RowDelegate(SetRowValueThreadSafe), new object[] { row });
return;
}
// now we're on the UI thread
object newValue = _rows[row];
row.Cells[0].Value = newValue; // randomly errors here with NullReferenceException, but row is never null!
}
Upvotes: 3
Views: 1391
Reputation: 35869
RE 1: The cardinal rule is that Windows controls must be accessed on the thread where their message pump is running. In .NET that's the thread that is running Application.Run
. It's the one processing the messages and everything you do to a Windows control is a message. So, if you send a message from another thread, the thread processing the messages could have race conditions (the message pump isn't thread-safe because that would impact performance too much, among other reasons). Now, accessing a .NET class instance doesn't necessarily cause a message to be sent. In those cases, yes, they can be accessed from a thread that isn't the UI thread. What does and does not send a message or under what circumstances a single member does and doesn't send a message is not documented. So, you're just gambling that an exception won't occur when you access a control object at all from a non UI thread. Best practice is to always use InvokeRequired
/BeginInvoke
. Anything else could change when you least expect it and if it does fail, what you did wasn't supported.
RE 2: I couldn't tell you if 1 caused 2--there really isn't enough detail. If you're doing something with something in an unsupported way, it's reasonable to expect unexpected things to occur.
If this is a WinForms app, I would recommend using Forms.Timer
instead. It invokes the Tick
handler on the UI thread and you don't have to bother with InvokeRequired
/BeginInvoke
Upvotes: 7