3

我知道您可以在不使用 Invoke/BeginInvoke 的情况下从工作线程读取 gui 控件,因为我的应用程序现在正在执行此操作。没有抛出跨线程异常错误,并且我的 System.Timers.Timer 线程能够很好地读取 gui 控件值(不像这个人:工作线程可以读取 GUI 中的控件吗?

问题 1: 鉴于线程的基本规则,我应该使用 Invoke/BeginInvoke 来读取表单控制值吗?这是否使它更加线程安全?这个问题的背景源于我的应用程序遇到的问题。似乎随机破坏了另一个线程正在引用的表单控件。(见问题 2)

问题 2: 我有第二个线程需要更新表单控件值,所以我调用/BeginInvoke 来更新这些值。好吧,同一个线程需要对这些控件的引用,以便它可以更新它们。它包含这些控件的列表(比如 DataGridViewRow 对象)。有时(并非总是),DataGridViewRow 引用会“损坏”。我所说的损坏是指引用仍然有效,但某些 DataGridViewRow 属性为空(例如:row.Cells)。这是由问题 1 引起的,还是您能给我一些提示,说明为什么会发生这种情况?

这是一些代码(最后一行有问题):

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!
    }
4

1 回答 1

7

RE 1:基本规则是必须在运行消息泵的线程上访问 Windows 控件。在 .NET 中,这是正在运行的线程Application.Run. 它处理消息,您对 Windows 控件所做的一切都是消息。因此,如果您从另一个线程发送消息,则处理消息的线程可能会出现竞争条件(消息泵不是线程安全的,因为这会过多地影响性能,以及其他原因)。现在,访问 .NET 类实例并不一定会导致发送消息。在这些情况下,是的,可以从不是 UI 线程的线程访问它们。没有记录什么发送和不发送消息,或者单个成员在什么情况下发送和不发送消息。因此,您只是在赌当您从非 UI 线程访问控件对象时不会发生异常。最佳做法是始终使用InvokeRequired/BeginInvoke. 其他任何事情都可能在您最不期望的时候发生变化,如果它确实失败了,那么您所做的就不受支持。

RE 2:我不能告诉你 1 是否导致了 2——确实没有足够的细节。如果您正在以不受支持的方式处理某事,那么预期会发生意想不到的事情是合理的。

如果这是一个 WinForms 应用程序,我建议使用它Forms.Timer。它调用TickUI 线程上的处理程序,您不必理会InvokeRequired/BeginInvoke

于 2012-09-07T16:14:41.417 回答