InvokeRequired大坑爹

.net下又写界面又写多线程的一定知道InvokeRequired(CheckForIllegalCrossThreadCalls党退散……)。我说个笑话,昨天被这东西坑了一下。……

正常情况下,.net创建的窗体、控件等对象,只允许通过创建它的线程进行访问。刚开始写多线程的人一定知道“线程间操作无效”这个异常,然后知道了InvokeRequired和Invoke。InvokeRequired按我理解,就是“当前线程 != 创建该窗体的线程”,所以当它为true的时候,要把对窗体的操作放到创建它的线程上去运行(就是调用Invoke)。

之前百度知道上有人求助关于聊天工具,那个时候我用UDP写了个P2P的简单示例,其中消息来了的时候检查发送来源,然后到一个Dictionary里去查找这个来源有没有对应的窗口,如果没有就new一个这对象出来,往窗口的TextBox里添加内容,然后托盘区闪动,点击的时候显示窗口。

“往TextBox里添加内容”这个我写了个方法,因为套接字上用了异步接收,收到后直接就检查有没有窗体和创建窗体这样的操作,所以创建窗口的线程其实是线程池里的线程。然后显示窗口的操作,因为托盘区图标的点击导致显示操作的执行,所以是主窗口所在线程,往TextBox添加内容的操作,因为是收到消息的时候调用(或者可能也包含发送消息的时候),所以是线程池的线程操作的(也可能是主窗口线程操作的),不过这个方法里我有检查InvokeRequired。

(这里插一句,我是承认在线程池的线程里创建窗口很不好啦……因为窗口的消息循环是跑在创建它的线程上的没错吧?但是当时这么写了,之后也没多注意)

然后可能是因为用了线程池的线程创建窗口和操作TextBox的缘故吧,调用Show的那边时不时会抛出异常(因为线程池的线程对于异常的处理貌似是……呃,那啥那啥那啥啥)。到昨天晚上,我改了代码,把创建这个窗体的动作拿到主窗体线程上去跑了,这样用户界面就都在同一个线程上创建和运行。尽管这样,调用Show其实也是在主线程上,但是Show的地方就是会抛异常,而且异常是说TextBox不能跨线程操作。这个时候我在给TextBox添加内容的函数上下了断点,然后发现,

虽然窗口new出来了,但是在窗口还没Show出来的时候,InvokeRequired返回的是false,尽管当前线程和创建它的线程不是同一个线程……

到搜索引擎搜索 InvokeRequired unreliable 关键字,看到也有其他人分遇到这样的问题了。囧

最后我的解决方法是InvokeRequired总是用主窗口的来判断,Invoke也用主窗口来Invoke。

总结就是,在窗口显示出来之前,InvokeRequired不可靠。

网上有提到Workaround的方法是new出一个窗口以后,想办法调用它的CreateHandle方法(protected的,不过可以在内部调用),使它确实是含有Handle的。InvokeRequired在这里会返回false的原因是窗口还没有Handle,我估计是第一次Show的时候它才真正创建窗口(懒汉式?)。不过这个workaround我没试过。

发表评论