最近在工作中写了一段代码,同事在看code review的时候也提出了一些意见,讨论下来觉得挺有意思。
意图:
在一个UI界面,有一个相对耗时的操作耗时的操作要做,所以才用了传统的backgroundworker来完成这个事情;同时为了用户体验,将鼠标置忙。在backgroundworker里面完成之后再设置为普通状态。
方案:
在页面的构造函数中,设置并启动backgroundWorker:
updateUserWorker.DoWork += (sender, e) => { ……; } updateUserWorker.RunWorkerCompleted += new BunWorkerCompletedEventHandler(Worker_RunWorkerCompleted);
updateUserWorker.RunWorkerAsync(); |
在Form load event handler里面设置鼠标为忙状态:
if (updateUserWorker.IsBusy == true) { this.UseWaitCursor = true; } |
在Worker_RunWorkerCompleted中将鼠标设置为普通:
try { ……; } finally { this.UseWaitCursor = false; } |
在code review中,同事提出,如果在form load event handler中,首先检查IsBusy为true,然后准备设置UseWaitCursor的时候,backgroundWorker完成了,请调用了RunWorkerCompleted,将UseWaitCursor设置为false,然后执行form load里面的this.UseWaitCursor = true;将会造成鼠标一直是忙状态。或者backgroundWorker会不会先调用completed,再设置IsBusy为false,这也会造成同样的问题。
初看来似乎颇有这个危险,但是后来仔细分析,应该确实没有这个问题。
- BackgroundWorker的completed event handler是在UI线程的callback。这个将会和Form Load Event handler在一个线程上执行。 如果线程正在执行Formload的IsBusy检查,应该是不会被callback抢断去执行completed event handler的。
- 透过反编译,我们可以看见backgroundWorker的内部实现为:
private void AsyncOperationCompleted(object arg) { this.isRunning = false; this.cancellationPending = false; this.OnRunWorkerCompleted((RunWorkerCompletedEventArgs)arg); } |
isRunning就是IsBusy的内部值,所以,是先设置了IsBusy然后再调用WorkerCompleted的。
Code review的疑问解决了,但是却给我带来了另外一个疑问:
一般来说,我们会用锁来保证一个代码段一次只有一个线程执行;那我们有办法保证一个线程在执行一个代码段的时候不会被打断么?这个也是有一点类似于对代码段的“原子操作”(只是类似,原来意义上的原子操作主要是针对一个变量的)。
比如说,在我们的这个例子中,如何保证在执行IsBusy检查之后,UI线程不会被抢断。或者,换一个说法可能更准确,在什么情况下,一个method执行到一半的时候会被暂停去执行另外一个method?
我想总体而言,应该是可以归纳为当线程处于空闲状态时,线程可能被唤起以执行completed callback。在sleep,suspend等状态均不可以。
- 比如,在使用MessageBox.Show 等待用户输入的时候。
- 在运行状态,callback不会被加入,必须等到运行结束执行。
不过,这些是我的一些观察结论,没有从根本上证明。
那么下一个问题是,如何知道一个线程处于空闲状态呢?可以通过代码解决么?
目前我还没找到,找到了过来update一下吧。