有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方法并不是一种推荐的方法,因为它可能会导致线程不安全的操作并引发不可预测的结果。相反,建议使用线程间通信来优雅地停止线程,例如使用共享标志变量或信号量等技术。