首页 > 解决方案 > 当UI中的事件被另一个线程中的对象触发而不调用跨线程操作异常时,如何处理它?

问题描述

我正在通过线程设计模式并试图解决一个小型实践项目。我得到一个 [InvalidOperationException: 'Cross-thread operation not valid: Control 'txtEventLogger' 从创建它的线程以外的线程访问。'] 当我的 UI 中的事件由另一个线程中的对象触发时调用方法在我的 UI 事件处理程序中,在文本框中记录一些事件。我怎样才能使这个线程安全?还是在使用线程时我使用了错误的设计模式来处理对象事件?

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

我有一个名为 Relay 的类,以及 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);
    }
}

标签: c#multithreadinguser-interfaceexceptionthread-safety

解决方案


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

因为您正试图访问不同线程上的控件,所以您只需通过调用 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/使用 postsharp-threading/ 简化多线程场景

推荐阅读