前言:
这是我在实际中发生的一件事,想告诉大家,如果不知道它的一些基本概念,一定要先测试一下自己想要的功能再去放到正式代码中用
切记切记
背景:
工具及程序:VS2022,WinForm程序,.net4.8
人物:小五(C#小白),领导(多年C#开发经验,但是很忙,此软件由领导开发完成初步功能)
正文:
某天上班的时候,小五发现了一个问题,于是......
小五:领导,我发现在这个TabControl下面有几页存在一些耗时操作,这些耗时操作会影响我们最重要的那个方法(此方法是被封装成dll文件的,轻易不改动,下面统称ImportentVoid)的执行啊,给你看看
(一波演示过后💻)
领导(大惊失色😲):那这个不行啊,这个必须要改,不然肯定会出问题的!
小五(抬头问道🤔):要不把这些耗时的放进一个新的线程里面?
领导(沉思😔):可以,之前这些代码(跳转到另外的代码了)用的是MDI窗口嵌入的方式,你可以试试这个,好像也是一个新的线程
小五:好的👌......
于是小五就去测试了,并没有去打印线程ID,而是直接放了一个for(int i=0;i<100000000;i++)
去模拟耗时操作,主窗体放ImportentVoid,子窗体放耗时操作,结果小五发现ImportentVoid直接就执行完了,看起来并没有影响,然后就得出结论:MDI窗口嵌入的方式,主窗体和子窗体是两个不同的线程(敲黑板!他们是同一个线程,这里要考)
小五(兴致冲冲😄):领导(繁忙中),我刚刚试了一下,MDI嵌入的方式,主窗体和子窗体是不同的线程
领导(点头道👍):好,那你就好那些tabpage页面迁到子窗体中,然后再测试看看
小五:好的
于是小五就开始了漫长的迁移工作
MDI方法使用:
//在主窗体的里的方法写
this.IsMdiContainer = true;
Form frm = new ChildForm();
foreach (Form childForm in MdiChildren)
{
childForm.Close();
}
frm.MdiParent = this;
frm.Show();
frm.WindowState = FormWindowState.Maximized;
迁移完成之后,小五又用for(int i=0;i<100000000;i++)
测试了一下,发现没问题,于是和领导报告之后就放到生产环境了
过了一段时间后,小五在执行耗时操作的时候,发现ImportentVoid方法并没有被执行,然后小李突然奇想,用while(true){}模拟耗时操作,哦豁,完蛋了😨,方法没有被执行,于是小五就去网上找到如何打印当前线程,然后找到了打印当前线程的方法:
Console.WriteLine("当前线程ID: " + Thread.CurrentThread.ManagedThreadId);
结果发现主窗体和子窗体的线程ID是同一个,于是小五赶紧去和领导报告
小五😑:领导,我刚刚又测试了一下,发现MDI嵌入的方式,主窗体和子窗体是一个线程
领导😶:一个线程啊,我看看
(一顿演示💻)
领导😕:那这样吧,你把那些耗时操作放到一个新线程里面,不过要抓紧了
小五(连忙点头😦):好的
于是小五又去借鉴怎么开新线程了,最后决定使用Task.Factory.StartNew(() =>方法())
,然后小五一顿操作,将第一个耗时操作(没有涉及到绑定到界面控件)放入新线程后,打印主窗体线程和新线程ID,发现他们是不一样的,于是大喜,又测试使用while(true){}模拟耗时操作,发现ImportentVoid能够被正常执行,然后就去迁移其他的耗时操作了,然后发现如果涉及到了绑定界面控件的耗时操作时就会报错“从不是创建此控件的线程操作它”,然后小五直接打开浏览器找寻方法
最后发现解决方法是这样的:
delegate void VoidInvoke(Dictionary<string, DataTable> _dataList);
//实际上调用的方法为RunImportentVoid
private void RunVoid(Dictionary<string, DataTable> _dataList)
{
VoidInvoke iv = new VoidInvoke(Void);
if (this.InvokeRequired)
{
Object[] parametors = new Object[] {
_dataList };
this.Invoke(iv, parametors);
}
else
{
Void(_dataList);
}
}
private void Void(Dictionary<string, DataTable> _dataList)
{
//耗时
}
于是小五继续更改,发现没有报错了,心里直呼终于好了,然后小五又把所有的耗时操作改了一遍,但是小五没有测试了,因为小五已经下意识的认为他们不是同一个线程了,改完后,小五发现,如果当一个耗时没有执行完的时候,用户打开了另外一个子窗体,就会报错,然后小五就想把在耗时操作执行之前让主窗体和子窗体的界面不可以点击,想到把Enabled属性改一下不就好了😁:
this.Enabled=false
this.Mdiparent.Enabled=false
想好之后,小五使用了ContinueWith来接受当前的耗时操作完成信号,最后的方法是
Task.Factory.StartNew(() => RunVoid(dataList))
.ContinueWith(o=>OnTaskCompleted(o));
在测试的时候,发现同样报错“从不是创建此控件的线程操作它”,于是继续和之前一样改造,改造完成之后,先进行正常测试,没有问题,于是来到了模拟耗时操作while(true){},哦豁,又完蛋了,ImportentVoid方法没有被执行,小五头大了😱,这哪里出了问题啊,于是小五在每一个方法里面都加了一个打印当前线程ID
最后发现是上面解决方案(“从不是创建此控件的线程操作它”)中的this.Invoke(iv, parametors);
出了问题,在执行这一步之后,子窗体中耗时方法的线程ID和主窗体的线程ID是一样的,小五直接原地炸裂🤡,于是小五苦笑😦找到领导,给他说明了情况,然后领导说,那只能改底层了......
总结:
1:MDI嵌入的方式主窗体和子窗体是同一个线程
2:Task.Factory.StartNew(() =>方法())是可以开一个新的线程并启动的
3:this.Invoke(); 会把线程强制拉到主线程中来
4:不清楚一个方法时,想要用它之前先测试!!!