C#委托和多线程问题

有form类Dbview,form类form1,线程类C。Dbview是主类,form1从Dbview启动(点击监控按钮),Dbview界面点击连接按钮后根据输入数据多少启动1到n个线程(线程方法实现根据线程类C)称这些线程为C1至Cn线程,C1到Cn使用线程类C的方法从opc循环获取数据,再通过2个委托传值给Dbview和form1。form1中用datagridview显示数据,使用方法getopcdata(getopcdata方法写在form1中)和线程类C中委托绑定,有数据就传送到getopcdata,再在方法体中修改datagridview的值。
现在一个问题是,我是用非创建datagirdview的线程修改datagirdview中表格的值(不使用invoke直接修改UI),并没有每次都弹出异常,而是极少次数报异常,这是为什么。
程序的使用是先点击连接按钮启动线程获取opc数据,然后点击监控按钮启动form1让获取的数据显示在form1上。
第二个问题是在使用invoke时,先点击连接按钮再点击监控可以正常显示,但是一旦关了form1,c1到cn线程就不再运行了,必须点连接重新启动c1-cn线程,但是不使用invoke直接修改UI时c1到cn就可以一直正常运行,这是为什么,如何在关闭form1之后继续使c1-cn正常运行
下面是在getopcdata中修改ui的代码(不使用invoke直接修改UI)

        public void getopcdata(Dictionary<string, Store> dictbuffer)
        {
            try
            {
                if (dictoutput.Count > 0)
                {
                    dataGridView1.RowCount = dictoutput.Count;
                    for (int i = 0; i < dictoutput.Count; i++)
                    {
                        foreach (var item in dictbuffer)
                        {
                            if (item.Key.ToString().Equals(dataGridView1.Rows[i].Cells[0].Value.ToString()))
                            {
                                dataGridView1.Rows[i].Cells[1].Value = item.Value.value;

                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString());
            }

当我使用跨线程Invoke的时候getopcdata方法如下

     public void getopcdata(Dictionary<string, Store> dictbuffer)
        {
            if (dataGridView1.InvokeRequired)
            {
                dataGridView1.Invoke(new MethodInvoker(delegate ()
                {
                    if (dictoutput.Count > 0)
                    {
                        dataGridView1.RowCount = dictoutput.Count;
                        for (int i = 0; i < dictoutput.Count; i++)
                        {
                            foreach (var item in dictbuffer)
                            {
                                if (item.Key.ToString().Equals(dataGridView1.Rows[i].Cells[0].Value.ToString()))
                                {
                                    dataGridView1.Rows[i].Cells[1].Value = item.Value.value;
                                }
                            }
                        }
                    }
                }));
            }
     }

线程类C代码

    public class OpcRead
    {
        public delegate void MyCallback(Dictionary<string, Store> dictbuffer);
        private MyCallback myCallback;
        private MyCallback form1Callback;

        public Dictionary<string, string> dict = new Dictionary<string, string>();
        public OPCUAHelper opc;
        public string root_url;

        public OpcRead(Dictionary<string, string> dict1, OPCUAHelper opc, string root_url, MyCallback callback,MyCallback callbackform1)
        {
            this.myCallback = callback;
            this.form1Callback = callbackform1;
            this.dict = dict1;
            this.opc = opc;
            this.root_url = root_url;
        }

        public void readopc()
        { 
           Dictionary<string, Store> dictbuffer=new Dictionary<string, Store>();

            while (GlassProcess.DbView.running)
            {
                if (dict.Count>0) 
                {
                    for (int i = 0; i < dict.Count; i++)
                    {
                        var name = dict.Keys.ElementAt(i);
                        var connection = dict.Values.ElementAt(i);
                        string threadname = Thread.CurrentThread.Name;
                        if (!connection.Equals("null"))
                        {
                            var value = opc.GetCurrentNodeValue(connection);
                            if (value == null)
                            { 
                                MessageBox.Show(name + "读取错误");
                                Store store = new Store(name,connection,threadname);
                                GlassProcess.DbView.dictretry.Add(name,store);
                                dict.Remove(name);
                                break;
                            }
                            else
                            {
                                Store store = new Store(name,root_url,threadname,value);
                                if (!dictbuffer.ContainsKey(name)) 
                                {
                                    dictbuffer.Add(name, store);
                                }
                                else if (dictbuffer.ContainsKey(name))
                                {
                                    dictbuffer.Remove(name);
                                    dictbuffer.Add(name, store);
                                }
                                
                            }
                        }
                    }
                    myCallback(dictbuffer);           //通过委托将dictbuffer发送给Dbview
                    form1Callback(dictbuffer);       //通过委托将dictbuffer发送给form1
                    dictbuffer.Clear();
                }
                Thread.Sleep(100);
            }
        }
    }
}

以下答案由GPT-3.5大模型与博主波罗歌共同编写:
关于第一个问题:为什么直接修改UI不会每次报异常?

首先,需要明确的是,UI控件只能由创建它们的线程直接访问和操作,其他线程必须通过控件的Invoke或BeginInvoke方法将操作委托给创建它们的线程。如果你直接在非UI线程修改UI控件,那么它不是安全的,因为不同的线程可能会在不同的时间修改相同的控件,这可能会导致控件状态的不确定性。

但是,在某些情况下,直接修改UI控件可能会工作。这可能是由于线程调度和控件状态之间的幸运组合。然而,这是一种不稳定的方法,不应该用于生产代码。应该始终遵守跨线程操作UI控件的最佳实践,即使用Invoke或BeginInvoke方法。

关于第二个问题:使用Invoke同时关闭form1后为什么C1到Cn线程不再运行?

这个问题是由于在使用Invoke调用UI线程操作时,如果UI线程已经结束(例如关闭了form1),那么调用可能会失败。因此,在使用Invoke方法时,应该检查是否可以访问UI控件,如果不能,则忽略控件更新并且不应该使任务失败。这可能是您的线程类没有这个逻辑,所以Invoke方法失败后线程对象退出。

因此,为了让线程类能够在关闭form1后继续运行,您需要修改getopcdata和OpcRead方法,使其能够在UI线程不存在时处理UI控件操作。以下是相应的修改代码:

getopcdata方法:

public void getopcdata(Dictionary<string, Store> dictbuffer)
{
    if (dataGridView1.InvokeRequired)
    {
        dataGridView1.Invoke(new MethodInvoker(delegate ()
        {
            if (!IsDisposed && !Disposing)
            {
                if (dictoutput.Count > 0)
                {
                    dataGridView1.RowCount = dictoutput.Count;
                    for (int i = 0; i < dictoutput.Count; i++)
                    {
                        foreach (var item in dictbuffer)
                        {
                            if (item.Key.ToString().Equals(dataGridView1.Rows[i].Cells[0].Value.ToString()))
                            {
                                dataGridView1.Rows[i].Cells[1].Value = item.Value.value;
                            }
                        }
                    }
                }
            }
        }));
    }
}

在方法中添加了检查UI线程对象是否正在被Dispose的逻辑,如果是,则忽略控件更新。

OpcRead方法:

public void readopc()
{ 
    Dictionary<string, Store> dictbuffer=new Dictionary<string, Store>();

    while (GlassProcess.DbView.running)
    {
        if (dict.Count>0) 
        {
            for (int i = 0; i < dict.Count; i++)
            {
                var name = dict.Keys.ElementAt(i);
                var connection = dict.Values.ElementAt(i);
                string threadname = Thread.CurrentThread.Name;
                if (!connection.Equals("null"))
                {
                    var value = opc.GetCurrentNodeValue<ValueType>(connection);
                    if (value == null)
                    {
                        MessageBox.Show(name + "读取错误");
                        Store store = new Store(name,connection,threadname);
                        GlassProcess.DbView.dictretry.Add(name,store);
                        dict.Remove(name);
                        break;
                    }
                    else
                    {
                        Store store = new Store(name,root_url,threadname,value);
                        if (!dictbuffer.ContainsKey(name)) 
                        {
                            dictbuffer.Add(name, store);
                        }
                        else if (dictbuffer.ContainsKey(name))
                        {
                            dictbuffer.Remove(name);
                            dictbuffer.Add(name, store);
                        }
                                
                    }
                }
            }
            if (!IsDisposed && !Disposing)
            {
                form1Callback(dictbuffer);       //通过委托将dictbuffer发送给form1
            }
            myCallback(dictbuffer);           //通过委托将dictbuffer发送给Dbview
            dictbuffer.Clear();
        }
        Thread.Sleep(100);
    }
}

在方法中添加了检查UI线程对象是否正在被Dispose的逻辑,如果是,则不需要调用form1Callback。

另外,您还需要在form1的Dispose方法中添加以下代码,在关闭窗口时清除对委托的引用,防止引起内存泄露:

protected override void Dispose(bool disposing)
{
    if (disposing && components != null)
    {
        // 清除委托引用
        dbviewCallback = null;
    }
    base.Dispose(disposing);
}

如果我的回答解决了您的问题,请采纳!

c#多线程和委托的使用

可以借鉴下
https://blog.csdn.net/qq_39559182/article/details/103954877

建议自己开发任何UI程序都遵循一下:后台线程不直接操作UI
C#的可以用SendMessage/PostMessage之类的方法,提交数据到主线程

引用chatGPT作答,
第一个问题可能是因为在使用非UI线程修改UI元素时,操作系统会对这些操作进行缓存,可能会让你看不到错误,但是这些错误仍然存在,并且可能在未来的某个时间点导致程序崩溃或数据损坏。

第二个问题是因为在使用Invoke方法时,委托操作会在UI线程上执行,因此在关闭form1时,UI线程也会退出,这样委托也就不能正常工作了。如果要在关闭form1之后继续使c1-cn线程正常运行,你可以尝试将委托方法放在另一个非UI线程上执行。具体地,你可以创建一个后台线程来执行委托操作,并在需要更新UI元素时使用Invoke方法来调用UI线程。同时,你需要确保该线程在应用程序退出之前被正确停止。

以下是一些参考代码:

首先,定义一个后台线程:

private Thread workerThread;

private void StartWorkerThread()
{
    workerThread = new Thread(() =>
    {
        while (true)
        {
            // 在这里执行委托操作,例如:
            // myCallback(dictbuffer);

            Thread.Sleep(100);
        }
    });
    workerThread.IsBackground = true;
    workerThread.Start();
}

private void StopWorkerThread()
{
    if (workerThread != null && workerThread.IsAlive)
    {
        workerThread.Abort();
        workerThread.Join();
        workerThread = null;
    }
}

然后,在form1的Load事件中启动后台线程:

private void Form1_Load(object sender, EventArgs e)
{
    StartWorkerThread();
}

最后,在form1的Closing事件中停止后台线程:

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    StopWorkerThread();
}

注意,使用Abort方法来强制终止线程是不安全的,因为它可能会导致线程在非确定性状态下停止,从而导致应用程序崩溃或数据损坏。更好的做法是使用一个标志变量来通知线程退出循环,并在循环中检查该标志变量,如下所示:

private bool stopWorkerThread = false;

private void StartWorkerThread()
{
    workerThread = new Thread(() =>
    {
        while (!stopWorkerThread)
        {
            // 在这里执行委托操作,例如:
            // myCallback(dictbuffer);

            Thread.Sleep(100);
        }
    });
    workerThread.IsBackground = true;
    workerThread.Start();
}

private void StopWorkerThread()
{
    stopWorkerThread = true;
    if (workerThread != null && workerThread.IsAlive)
    {
        workerThread.Join();
        workerThread = null;
    }
}

不使用invoke直接修改UI时c1到cn就可以一直正常运行
这是因为它实际上修改的是窗体的属性,而Invoke实际上是通过消息同步的,而窗体关闭,消息循环就关闭了
此时窗体的 IsDisposed 就为 true

引用new bing部分回答作答:
首先回答第一个问题:您使用非UI线程修改DataGridView中的数据,有时不会引发异常的原因是因为这种情况下,当DataGridView控件未被锁定时,非UI线程可以更改控件的属性,因此并不会出现每次都弹出异常的情况。但是这种做法是不推荐的,因为它可能会导致UI控件的不一致和意外的结果,例如显示错误的数据或崩溃等问题。

其次回答第二个问题:在关闭form1之后,如果使用Invoke调用UI线程来修改UI控件,那么线程将无法正常运行,因为Invoke调用需要有一个UI线程来处理,而form1已经被关闭,因此Invoke将无法找到UI线程。为了让c1-cn线程继续运行,您需要将它们创建为后台线程(即将线程IsBackground属性设置为true),这样即使主线程(即UI线程)退出,这些后台线程仍然可以继续运行。您可以在线程启动时将IsBackground属性设置为true,如下所示:

Thread thread = new Thread(new ThreadStart(YourThreadMethod));
thread.IsBackground = true;
thread.Start();

此外,您也可以在form1关闭时停止所有线程,可以使用Thread.Abort方法来终止线程。然而,使用Thread.Abort方法并不是一种推荐的方法,因为它可能会导致线程不安全的操作并引发不可预测的结果。相反,建议使用线程间通信来优雅地停止线程,例如使用共享标志变量或信号量等技术。