问一个跨线程调用控件,BackgroundWorker执行时让我疑惑的问题

zpc38368330 2015-05-21 10:57:20
原文地址:http://www.cnblogs.com/TankXiao/p/3348292.html#backgroudworker

private void button4_Click(object sender, EventArgs e)
{
using (BackgroundWorker bw = new BackgroundWorker())
{
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
bw.DoWork += new DoWorkEventHandler(bw_DoWork);
bw.RunWorkerAsync("Tank");
}
}

void bw_DoWork(object sender, DoWorkEventArgs e)
{
// 这里是后台线程, 是在另一个线程上完成的
// 这里是真正做事的工作线程
// 可以在这里做一些费时的,复杂的操作
Thread.Sleep(5000);
e.Result = e.Argument + "工作线程完成";
}

void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
//这时后台线程已经完成,并返回了主线程,所以可以直接使用UI控件了
this.label4.Text = e.Result.ToString();
}

我做了这样一个实验
button4_Click此代码是在某子窗体中执行
 using (BackgroundWorker bw = new BackgroundWorker())
{
.....
} //将子窗体完全关闭
this.Close();
this.Dispose();



bw_DoWork在此后台函数内什么也不做只是Sleep(5000),休眠了5000毫秒


RunWorkerCompleted在此函数内再次添加
this.Close();
this.Dispose();
[/code]

照我抄的这段代码内的注释,RunWorkerCompleted此函数回到了主线程,
可是主线程(子窗体)的代码内,我将其自身已经完全关闭,并且释放,理论上,是否此处因该有异常访问?访问了已经释放的空间?
难道仅是我运气好,程序在RunWorkerCompleted内没有崩溃?
...全文
282 12 打赏 收藏 转发到动态 举报
写回复
用AI写文章
12 条回复
切换为时间正序
请发表友善的回复…
发表回复
zpc38368330 2015-05-21
  • 打赏
  • 举报
回复
如下代码,以前从没试过,不过居然也没有问题....
this.Close();
 this.Dispose();
 GC.Collect();
 this.Close();
this.Dispose();
 this.textBox_LoginName.Text = "aa";
但是下面两个例子,出了问题
            BeginInvoke(new MethodInvoker(
            () =>
            {
                Thread.Sleep(5000);//这里没有运行,是否因为窗体已经释放了,所以此线程也被停止了
                this.Close();
                this.Dispose();
                this.textBox_LoginName.Text = "aa";
            }));           
            this.Close();
            this.Dispose();
            GC.Collect();
            this.Close();
            this.Dispose();
            this.textBox_LoginName.Text = "aa";
            this.Close();
            this.Dispose();
            GC.Collect();
            this.Close();
            this.Dispose();
            this.textBox_LoginName.Text = "aa";
            BeginInvoke(new MethodInvoker(  //程序执行到此处,发生了异常- -
                                                                          //异常信息是:在创建窗口句柄之前,不能在控件上调用 Invoke 或 BeginInvoke。
            () =>
            {
                Thread.Sleep(5000);
                this.Close();
                this.Dispose();
                this.textBox_LoginName.Text = "aa";
            }));
看来确实是BackgroundWorker的方法,对跨线程调用控件进行了保护,所以即使窗体执行了dispose也依然未完全释放,这也是原文作者对BackgroundWorker跨线程调用控件的强烈推荐的原因吧
  • 打赏
  • 举报
回复
引用 5 楼 zpc38368330 的回复:
这个~~如果我又调用了GC后,.net的实现能保证 "既然窗体里还有对象在保持对窗体的强引用,窗体就不会被回收 "这个实现的正确性吧? ---又内存泄露了----
.Net能保证,但前提是,你是在构造函数中new的对象,并且添加到控件或者组件窗器中,如 this.Controls.Add(ctrl); var timer = new Timer(this.componet); 当Form中没有使用组件时,你会发现componet并不会初始化 这两种在窗体关闭时都能释放,但有一种必须要你自己释放,比如你点个按钮,然后在代码中new timer()需要你显式调用Dispose来释放,这主要是因为timer使用到了非托管资源(一部分内存不是由CLR和GC来管理,而是像C++那样直接向系统申请的内存) 一个全局引用的sqlConnection,在dispose()后,会不会出现异常?只能说在一个很短的时候内不会,还是跟上面说的一样,跟GC释放它的时机有关。一旦释放后,不管是当前线程还是别的线程访问它都将是空引用。 MSDN的建议是不要使用dispose()过的对象,一旦你用了,就意味着你已经明确表示不需要再使用它了 GC回收是有策略的,回收的时候默认情况下挂起其他的线程(比较影响性能),所以回收不会是实时的。你可以去查查资料
zpc38368330 2015-05-21
  • 打赏
  • 举报
回复
我只是有点疑问,如果dispose执行的是释放组件所使用的所有资源(这是vs编辑器里提供的原话),那么就应该去释放,此情况我碰到过,一个全局的变量被某线程dispose后,如sql的connect连接,然后另一个线程访问,就会出现异常.这按照逻辑是应该发生的事. 但是在这个例子里,却没有发生按理说该的发生的事.所以我有疑问,如果万一哪天新版本的C#的思路改变,,认为所有的dispose就该执行其所应当执行的事,即便出现问题,那也是程序员设计的不合理,那这段代码就会带来很多的问题.
tcmakebest 2015-05-21
  • 打赏
  • 举报
回复
由此可见窗体并没有真正被释放, 所以才不会有任何异常. 研究了下 BackgroundWorker 这个类,执行完成事件的代码好复杂都看晕了.
zpc38368330 2015-05-21
  • 打赏
  • 举报
回复
哦.理解了... 我愚蠢的以c++的想法去考虑.net了 不过我还可以更愚蠢一点 这个~~如果我又调用了GC后,.net的实现能保证 "既然窗体里还有对象在保持对窗体的强引用,窗体就不会被回收 "这个实现的正确性吧? 嘿嘿~搞了多年的C++对一切指针,内存什么的都有强烈的怀疑能力,总是怕漏点什么...特别是搞点什么非windows平台的开发,简直会搞出什么指针内存的心理恐惧症~~ ---又内存泄露了----
  • 打赏
  • 举报
回复
访问this对象的直系成员一般不会报错,比如this.datagridview 但是如果你将this.Close()改成this.datagridview.Rows.Add(1, "abc");多半会报错 为什么说多半?因为这是机率事件,与控件内存实际释放的时机有关(由GC控制) 而this.Close()是不会的,因为它里面调用的是Dispose(bool isDisposing),实际上只会标记需要回收,多次调用没有意义 RunWorkerCompleted是事件(委托),由UI线程来调用的。 从windows消息循环的角度来讲,当窗体关闭时,实际上调用了PostQuitMessage(发送了WM_DESTROY消息),来终止此窗体上的消息循环,你对this的所有操作几乎都离不开它,它停了,你的一切调用也就不再有机会得到执行 另外,如果该窗体是主窗体,this.Close()也就终止了此程序的前台主线程,程序也就退出了(无视BackgroundWorker后台线程,也就是说,还没等你sleep(5000)完,程序就退出了
  • 打赏
  • 举报
回复
跟操作系统销毁对象占用的内存空间是完全不相关的 -> 跟.net底层运行时系统销毁对象占用的内存空间是完全不相关的 真正去销毁空间那是GC的事儿。许多人把 Dispose 误认为成是GC操作了。
於黾 2015-05-21
  • 打赏
  • 举报
回复
this.Close(); 窗体关闭,但是并不代表实例被销毁 this.Dispose(); 告诉GC,该对象可以销毁,但是不代表会立即被回收 既然窗体里还有对象在保持对窗体的强引用,窗体就不会被回收
  • 打赏
  • 举报
回复
啥叫“已经释放的空间”呢? 很多人很空洞地理解“释放”这个词儿,简直就跟上山拜佛一样。而你问他“释放了什么?”,他无言以对,根本不知道,也从来没有看过Dispose的源代码。他的理由就是“反正释放了就不应该再能够被访问了”。 托管程序,其Dispose概念跟操作系统销毁对象占用的内存空间是完全不相关的。Dispose就是相当于“吃完饭唰唰碗筷”而已。所以不要胡乱滥用“释放”这个词儿,它可不是“摔碎碗筷掰断筷子”的意思。
zpc38368330 2015-05-21
  • 打赏
  • 举报
回复
using (BackgroundWorker bw = new BackgroundWorker())
                {
                    bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
                    bw.DoWork += new DoWorkEventHandler(bw_DoWork);
                    bw.RunWorkerAsync(argument);
                }       
            this.Close();
            this.Dispose();
            GC.Collect();
            this.Close();
            this.Dispose();
            this.textBox_LoginName.Text = "aa";
原测试代码,已更改为正常的代码,这仅算是个模拟的代码 bw_DoWork //后台执行的代码 { thread.sleep(5000)//这里加了断点,此处确实休眠了5秒 } bw_RunWorkerCompleted//完成后台后,返回主线程(子窗体内)的代码 { GC.Collect(); this.Close(); this.Dispose(); this.textBox_LoginName.Text = "aa"; } 此段代码,什么问题也没有,全部正常执行,并且 this.textBox_LoginName.Text 也被正确赋值,(不过我没试在父窗体内,再读取textBox_LoginName.Text 会发生什么,理论上当然会非法访问,所以没有去测试) BackgroundWorker正常执行了,并且与BeginInvoke(new MethodInvoker(...)对比,很明显,BackgroundWorker执行时对子窗体资源的释放进行了保护, 查看我提的3个例子的最后一个例子的异常即可知道 ex.tostring()=在创建窗口句柄之前,不能在控件上调用 Invoke 或 BeginInvoke。
zpc38368330 2015-05-21
  • 打赏
  • 举报
回复
引用 10 楼 dongxinxi 的回复:
个人觉得你这两个例子并没有本质区别,都是“不安全”的 beginInvoke用的托管线程池线程(后台线程),只有当所有的前台线程都终止,后台线程才会自动终止 所以要看你的this窗体是否就是主窗体,也就是说,this.Close()之后程序是否退出了? 一旦你用了Dispose(),就意味着你已经明确表示不需要再使用它了,在此之后的任何地方你都不应该再去访问此对象 想想C++中你free过后,会再次使用那个对象吗?(在不重新赋值的前提下) 只要你记住这个原则就行 BackgroundWorker的另一个好处是,还封装了取消机制 通过调用asyncCancel()来取消先前的异步操作,你只需要在RunWorkerCompleted事件中,判断 if(e.Cancelled) { return; }
哦,情况是这样的,我是在主窗体new了一个dialog,在此dialog的一个按钮里执行的上述代码 所有的this都是子窗口的 我所疑惑的地方也正是你所描述的认为正确的解答 我最后提供的3个例子,都是按一个程序设计的角度该会正常发生的事情. 但是如果换成BackgroundWorker,既将
BeginInvoke(new MethodInvoker(//此段代码替换为BackgroundWorker的,让其在后台执行相关跨线程调用控件
            () =>
            {
                Thread.Sleep(5000);//这里没有运行,是否因为窗体已经释放了,所以此线程也被停止了
                this.Close();
                this.Dispose();
                this.textBox_LoginName.Text = "aa";
            }));           
            this.Close();
            this.Dispose();
            GC.Collect();
            this.Close();
            this.Dispose();
            this.textBox_LoginName.Text = "aa";
(源代码已经全部修正成正常的逻辑代码了,所以未贴详细的测试代码) 如上更改代码后,则不会出现异常,而且我跟踪代码,各处代码均有正确的值,而不是null,或者说没有被GC掉,(并且我还在BackgroundWorker后台执行委托里Sleep了5000毫秒),也就是说子窗口的这个this在BackgroundWorker的相关执行中,主线程(子窗体)一直没有被释放,或许就是因为有了强引用,.net保护了子窗体,并没在其GC dispose后释放,(这按C++的思路,是该出错的,并且调用BeginInvoke(new MethodInvoker(...)也确实出错了,但是BackgroundWorker没有出错,我疑惑或者说惧怕的原因就是在这里)
  • 打赏
  • 举报
回复
个人觉得你这两个例子并没有本质区别,都是“不安全”的 beginInvoke用的托管线程池线程(后台线程),只有当所有的前台线程都终止,后台线程才会自动终止 所以要看你的this窗体是否就是主窗体,也就是说,this.Close()之后程序是否退出了? 一旦你用了Dispose(),就意味着你已经明确表示不需要再使用它了,在此之后的任何地方你都不应该再去访问此对象 想想C++中你free过后,会再次使用那个对象吗?(在不重新赋值的前提下) 只要你记住这个原则就行 BackgroundWorker的另一个好处是,还封装了取消机制 通过调用asyncCancel()来取消先前的异步操作,你只需要在RunWorkerCompleted事件中,判断 if(e.Cancelled) { return; }

110,534

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧