为了线程安全,winform和wpf框架中规定只能使用UI线程操作控件,从其它线程上操作控件就会报跨线程异常。假如有这样一个场景:点击按纽,然后开始计算员工薪资,并将计算信息实时展示在一个文本框中,由于计算过程比较耗时,为了不让界面卡死,我们会将计算方法放在单独一个线程中。UI代码如下:
薪资计算类代码如下:
上面的代码就不过多解释了,相信大家都看的懂。按纽点击后,开启一个新线程,执行计算,并将更新UI的方法以委托的形式传给SalaryCalculator类。我们执行一下,如下:
不出意外,报错了,我们不能在新线程中更新UI线程。一般的做法,我们可以使用Invoke,这个大家应该都用烂了。改写下ShowMessage,代码如下:
除了以上办法,我们还可以使用SynchronizationContext来解决上面的问题。这个类,大家可能比较陌生, 我们来看一下它的定义,如下:
它的定义:提供在各种同步模型中传播同步上下文中的基本功能。其实它的含义就是对当前线程上下文的封装,或者叫当前线程所在环境的封装。封装的对象可以传递至其他线程,然后在其他线程中调用其Post或Send方法,以此来实现线程间的消息传播。我们使用SynchronizationContext修改上面的代码,得到的结果都是一样。代码如下:
上面的代码中,先通过SynchronizationContext.Current获取UI线程的同步上下文对象,然后在计算薪资的线程中使用这个对象的Post方法,这时控制是在UI线程的上下文中执行,所以不会报错。
讲到这里,其实SynchronizationContext的内容就讲完了,不过有个点可以再补充下。大家应该知道Task对象有一个ConfigureAwait()方法,用来配置是否同步上下文,我们到这个方法中看一下,代码如下:
continueOnCapturedContext尝试将延续任务封送回原始上下文件,默认为true。这里说的原始上下文,其实就是SynchronizationContext,即异步前(await)前所在的线程的同步上下文。我们将Calculate改成异步方法,代码如下:
我们知道,异步方法在遇到await之前都在当前线程中执行,当执行完await这行后,方法就会退出,然后会将await之后的代码封装成委托(可能不太准确,大概这个意思,会产生一个状态机类,不展开讨论)。在执行await时,默认会捕获当前的线程上下文,然后当执行完Task.Delay(1000)后,上面说的线程上下文就会将剩下的代码发回(Post/Send)自己的线程执行。大概像下面这个样子,代码如下:
上面代码不一定准确,只是想表达这个意思。如果我们不想剩下来的代码在原来的上下文中执行,可以将continueOnCapturedContext设为false,这也是微软推荐的做法。不然会出现一些意想不到的情况,比如死锁。我们看一下调用的地方,代码如下:
我们配置了不捕获上下文,这时代码是正常运行的。我们再来演示一下经典的死锁问题吧,如果你还有兴趣就接着向下看吧。我们改造下上面的代码,改成同步等待,并默认捕获上下文。代码如下:
我们定义task变量,并去除ConfigureAwait(false),这样在Calculate中默认会捕获上下文。下面的task.Wait()会等待task完成,可是我们在线程上下文中又会执行Post方法,这时互相等待,造成死锁。解决办法:用到异步的地方都加上ConfigureAwait(false),另一个不要使用Wait方法,用异步就异步到底。
终于讲完了,今天讲的内容还是很简单,如果能帮到你一点点,我就会很开心的(能关注下就更好),哈哈。
最后PS一下这个demo的界面图吧,让你们看看我的设计能力~~