UI中的事件被另一个线程中的对象触发时如何处理而不调用跨线程操作异常?

How do I handle an event in UI when it's triggered by an object in another thread without invoking a cross-thread operation exception?

我正在研究线程设计模式,但一直在尝试解决一个小的实践项目。我得到一个 [InvalidOperationException: 'Cross-thread operation not valid: Control 'txtEventLogger' accessed from a thread other than the thread it was created on.'] 当我的 UI 中的事件由我的对象触发时另一个线程从 UI 事件处理程序调用我的 UI 中的方法,以在文本框中记录一些事件。我怎样才能使这个线程安全?还是我在使用线程时使用了错误的设计模式来处理对象事件?

我的 GUI WinForm 有 3 个文本框和 1 个按钮:

我有一个 class 命名为 Relay,还有另一个 class 派生自 EventArgs CustomEventArgs 用于事件处理。

public class Relay
{
    //CustomEventArgs, derived EventArgs class to hold object index and a string message
    public delegate void OnSend(object sender, CustomEventArgs e);
    public event OnSend Sent;

    public int index;
    public int Index {get => index; set => index = value; }
        
    public void Send(string UserMessage)
    {
        //TODO: Meanwhile trigger event for testing

        //EventArgs class not shown, but it takes:
        //CustomEventArgs Constructor  CustomEventArgs(int index, string message)
        Sent(this, new CustomEventArgs(this.index, UserMassage.Length.ToString()));
    }

}

我的 WinForm 代码:​​

public partial class Form1 : Form
{
    private void btnStart_Click(object sender, EventArgs e)
    {
        // create a couple threads
        for (int i = 1; i < 3; i++)
        {                                               // txtAmountOfMessages = 3
            new Thread(() => CreateRelaysAndSendMessage(Convert.ToInt32(txtAmountOfMessages.Text), txtMessageToSend.Text)).Start();
        }
    }

    private void CreateRelaysAndSendMessage(int AmountOfMessages, string Message)
    {
        List<Relay> relayList = new List<Relay>();

        // Create 5 objects of Relay class, index, and subscribe to ui eventhandler
        for (int i = 0; i <= 4; i++)
        {
            relayList.Add(new Relay());
            relayList[i].Index = i;
            relayList[i].Sent += RelaySent;
        }

        // For each object, call .Send, 3 times (from txtAmountOfMessages value)
        foreach(Relay myRelay in relayList)
        {
            for (int j = 1; j <= AmountOfMessages; j++)
            {
                myRelay.Send(Message);
            }
        }
    }

    private void RelaySent(object sender, CustomEventArgs e)
    {
        // Exception handling error here
        Log("Relay number " + e.Index.ToString() + " sent msg " + e.Message);            
    }

    public void Log(string Message)
    {
        // Exception handling error here
        //System.InvalidOperationException: 'Cross-thread operation not valid: Control 'txtEventLogger' accessed from a thread other than the thread it was created on.'
        txtEventLogger.AppendText(Message + Environment.NewLine);
    }
}

对于其他人来说,这是一个相对直接的答案。

因为您正试图访问不同线程上的控件,您只需通过调用 BeginInvoke 将它们编组回来。这其实很简单!

改变这个:

txtEventLogger.AppendText(Message + Environment.NewLine);

对此:

txtEventLogger.BeginInvoke((Action)(() => txtEventLogger.AppendText(Message + Environment.NewLine));

Begin Invoke 只是告诉 .NET 你想在 UI 线程上调用它(你应该做什么,以及错误试图告诉你什么)。

与委托一起工作可能会很痛苦,但如果您想在 Windows 表单中进行多线程工作,这就是生活的一部分。其他选项是:

  1. 使用 Windows 调度程序(搜索 System.Windows.Threading.Dispatcher),尽管这实际上与在您的控件上调用 BeginInvoke 本质上是一样的。
  2. 使用具有编组调用帮助程序的线程库。通常,这些允许您只用一个属性来装饰方法,这样说“嘿,我想要这个 运行 在 UI 线程上,请”,它会完成剩下的(更多信息在这里 https://dotnetcoretutorials.com/2020/12/10/simplifying-multithreaded-scenarios-with-postsharp-threading/