从异步更新进度想起的事儿——IProgress
今天,在群里向大家请教了这样一个问题:“两个对象(类、窗体或什么)之间,要完成比较频繁的报告进度更新都有哪些好的方式”,Somebody 跳出来给出了个“IProgress”,没了解过,后面围绕着它讨论学习了下。
简单来说,IProgress 是类库给出的一种解决问题的方式而非具体实现,IProgress 包括一个需要实现的 Report 方法。使用时由调用“任务”的“创建者”创建对接口的实现,也就是具体实现 Report 的方法,此时创建者便可以具体控制要如何报告、报告什么,例如在 Report 中写上这么一句简单粗暴欠揍的“ textBox1.Text = currentProgress.ToString()”,但创建者控制不了的是“什么时间报告”,因为这是由“任务”本身设定的。创建者将这个具体实现传递给“任务”,任务在其逻辑内(某个属性内或某个方法内)加上一句 IProgress.Report(xxx),实现了“什么时间报告”。
在这个逻辑中,创建者可以自由控制并专注实现如何报告,而不用关心何时接到报告(事实上,创建者也没法关心,因为何时调用 Report 方法是任务决定的,并且创建者也不必亲自“接收”报告,因为报告的方式创建者已经实现了:textBox1...);而任务专注于何时报告进度更为合理,而不用关心怎么样报告,只要在需要报告的时候调用 Report 方法就好。好处就是各管各的,看似比较合理。
Somebody Ivony 还是提醒了这种方式类似于事件的隐患,即“调用了IProgress接口后,任务抓住了IProgress对象的引用,这一点和事件是一样的,所以应当设计轻量级的IProgress对象,并且与Worker的调用方解耦”。
感谢大家和 Ivony 的解答!
试试手气 9:45:05 请教大家个事儿,如果有个类暂且叫做 worker,这个 worker 要完成一些工作(不用管具体干什么),重要的是他要对工作的进度做频繁的更新和报告(没有频繁到高大上的地步),要使其它类或对象了解 worker 的进度都有哪些比较好的机制,worker自身事件?让其它类访问 worker 的公开只读属性? 上次Ivony说到事件的坑,就想到了这个事儿 ♪慕斯⌒。²º¹⁴ 9:48:45 Ivony 说的是.NET内部控件的事件是个坑吧。 试试手气 9:50:07 控件的事件和自定义事件有区别吗? X 9:52:32 有些细节别太在意 试试手气 9:53:35 那好,那我就向大家请教一下都有哪些机制可以完成任务吧 ^^ Ivony... 9:54:24 IProgress 试试手气 9:58:25 让 worker继承 IProgress 吗? Ivony... 10:01:49 不是,你可以在MSDN上搜索相关示例。。。。 试试手气 10:01:59 我正在查看 IProgress,Ivony Ivony... 10:02:39 IProgress接口的用法是由调用任务的程序创建,任务会调用这个接口的方法来反向通知进度。 试试手气 10:38:36 void Factory() { var mem = new Worker(); mem.Work(new IProgress<int>()); } pubulic class Worker{ void Work(IProgress<int> progress) { 工作逻辑(); progress.Report(i++); // 报告工作 } } 试试手气 10:45:51 我刚才写的那个伪代码的逻辑对么,是你说的意思吗? Ivony... 10:46:09 是的,微软推荐的方案就是这样。。。。 不过这个你也可以仅做参考,这样反转通知是避免事件陷阱的一种方式。 Ivony... 10:47:20 因为反转之后,变成了Worker依赖于调用方,而调用方肯定比Worker活得久,所以自然没有属性的问题。 因为反转之后,变成了Worker依赖于调用方,而调用方肯定比Worker活得久,所以自然没有事件的问题。 而且IProgress<T>是可以与调用方解耦的,互相没有牵连。 倉ル 10:48:41 Ivony... 10:49:49 我好像想错了,刚才的请忽略,我要开会去了。。。。 试试手气 10:50:26 但我感觉里边好像缺少了一个我还没理解的,非常关键的 key 的东西,就是 怎么就“报告进度”了。请忙 倉ル 10:51:30 好像是在你 work 里工作的每个阶段 调用report 吧 试试手气 11:01:38 Report 是 IProgress 接口的唯一一个方法,就是用来报告进度的。什么时候调用 Report 不是问题,我还没理解 Report 方法怎么就向其父级 Factory 报告进度了?还是……难道……莫非……Report 并不是向 Factory 作报告的 倉ル 11:03:49 像谁报告 你是说的算的吧 你在调用work 的时候指定 报告接收者的吧 X 11:04:16 应该是取决于你怎样实现IProgress这个接口的 Ivony... 11:09:25 是的,,,,, 譬如说你可以把IProgress绑定到一个进度条上。 试试手气 11:17:57 恩,X 说的是我还没融会贯通的节点……let me see see 1 -> IProgress 既然是个接口,那使用之前应该先去实现它 2 -> 实现IProgress的对象,里边需要实现 Report 方法,所以,方法里边会明确写明“如何更新”,比如Report{ textBox1.Text = i.ToString();} 3 -> Factory 把自己创建的实现接口的这个对象给到 Worker,让 Worker 来报告进度 所以,Factory 不用管什么时候报告,是由 Worker 决定的;而 Worker 不用管具体 Factory 需要如何报告,只知道自己调用 Report 方法,Factory就能收到了 Right? Ivony... 11:19:02 Right。 你想一下,其实这个和事件是一样的。 试试手气 11:19:16 对,很类似 Ivony... 11:19:21 也就是说事件和这种类型的接口是等价的。 我之前说错了是这种结构其实没有从根本上解决事件的内存泄露问题。 Ivony... 11:21:03 回想一下事件的内存泄露怎么造成的,事件宿主会抓住所有挂接的委托的引用,委托抓住方法所属实例的引用。而事件宿主不能尽快释放就会挂掉。 用IProgress接口后,Worker抓住了IProgress对象的引用,这一点和事件是一样的,所以应当设计轻量级的IProgress对象,并且与Worker的调用方解耦。 试试手气 11:22:55 但有个问题,假如 Factory 是个窗体,需要更新进度显示在 textBox1 就像我刚才举的例子。而 Worker 是个类,假设 Factory 定义的 Report 方法里写上 textBox1.Text = xxx ,这不就是个很大的问题吗? 你的意思是,关键在于如何实现一个轻量级、解耦的 IProgress 实现。 Ivony... 11:25:17 Report方法里面写什么无关紧要,关键是你的IProgress实现类的字段。 这个类型的实例会被Worker抓住,所以其字段在Worker被释放前都不能释放。 其实呢你的Worker如果生命周期很短的话是不用考虑这个问题的。 Ivony... 11:26:20 事件的陷阱主要出现在那个Timer上,这货是个无限生命周期的东西。 属于设计失误。 试试手气 11:28:10 但假如 worker 是个周期长,长时间后台运行的东东就会增加风险 Ivony... 11:28:37 是的,,,, 试试手气 11:30:16 但很多情形,worker 都很可能是个周期长、长时间后台运行的东东。比如后台持续下载队列中的文件、持续读取或写入磁盘等 这时候,是不是就需要对 worker 自身重新审视和设计了 Ivony... 11:30:55 嗯,,,,其实有这个意识就好了,过分的纠结于这一点反倒无益。 长和短都是相对而言的。 即使生命周期很长,如果只挂一个Progress上去也没事,你不停的挂才会有事。 试试手气 11:31:51 嗯 Ivony... 11:31:57 事件的问题就在于大家经常无止尽的挂事件上去,从来不解挂,,,, 举另一个例子吧,,,, 创建一个WebClient,使用基于事件的异步。。。。 Ivony... 11:33:04 你先挂一个方法在Completed,然后导航到某个地址,加载完后会引发Completed事件。 然后你又挂一个方法到Completed,再导航,,,, 如此反复,挂了,,,,, X 11:35:37 事实上应该很少有人会对同一个对象重复注册多个相同的事件吧 Ivony... 11:36:52 Timer就会啊,,,, X 11:37:50 Timer也是只注册了一次吧 试试手气 11:45:18 去记录下这次的对话
哦对了,在博客园找到一篇比较直观的帖子作为推荐,作者 Saar,点击查看 => 帖子原文
点击查看 => 来自 Microsoft Blog 的帖子