很为中国人伤心,因为这是一片人云亦云的地方。为什么?我问大家一个问题Monitor.Wait和Monitor.Pulse分别是什么意思?
这个时候大概会有一半的人查MSDN或者Google一下,然后就回答:
Monitor.Wait()是 释放对象上的锁并阻塞当前线程,直到它重新获取该锁。
Monitor.Pulse()是 通知等待队列中的线程锁定对象状态的更改。
如果按照MSDN上面的解释,Wait应该会释放对象上的锁,而Pulse则跟释放这个锁没有什么关系。事实上我们可以看一下MSDN提供的一个标准的例子,这个例子在Pulse函数的帮助里面就可以找到。大家先不用看代码,直接运行。屏幕上面是否出现了1...1000这样字眼呢?然后程序正常结束退出。现在我们来修改代码,如果Wait确实是MSDN上面所说的“释放对象上的锁并阻塞当前线程”,那么就应该会释放这个锁啊,那么我们把所有的Pulse去掉看看。一运行,死掉!
那么就是说,wait一定要有其他线程的pulse才能够激活,并且在wait之前需要用pulse使得其他线程的wait能够激活,缺少任何一个都会造成死锁。我又来问一下,真的是这样的吗?如果你真的这么认为的话,那要么是MSDN错了,要么是你错了。我们仔细看一下pulse函数的帮助里面的一句话:
备注
只有锁的当前所有者可以使用 Pulse 向等待对象发出信号。
当前……
请注意,……Wait(Object, Int32) 的备注说明在 Wait 之前调用 Pulse 时引起的问题。
若要……
也就是说如果你在wait之前调用pulse可能会引起问题?完了,这是什么问题呢?我们只好接着翻开Wait(Object, Int32)看备注了。大家努力找找看,我是找不到任何关于这方面的说明。好了,现在我们要思考一个问题了,假如实际上在wait之前调用pulse并不会引起伤害,或者对于某些特殊情况可能达不到某种意图,为什么Wait不自动发出一个Pulse,而非要我们来手动发出一个Pulse呢?实际上你在MSDN的例子里面,在FirstThread的第一个Wait之前添加一个Pulse,并不会引起什么问题啊。也就是说,我们实际上在任何一个Wait之前都可以(甚至绝大多数的情况下都必须)调用一次,甚至分开来写和连续一块写效果是完全一样的。eg:
Pulse();
...
... // 你可以在这里试一下任何你能够想得到的强行线程调度的方法。
...
Wait(...);
和
...
...
...
Pulse();
Wait(...);
从执行顺序和执行结果来说都是完全一样的,如果不相信的话,你尽可以试一下调度线程。我已经做过一定的实验了,结果就是“似乎没有任何线程被调度”,或者很可能是那些被阻塞的线程确实调度到就绪队列然后调度到运行队列里面,只是这时候还处在Montior.Enter/TryEnter/Wait里面,他们一检查发现锁并没有被释放,于是又回到就绪队列里面,一直等到下一次进入运行状态再做检查。但是把pulse和wait这两个功能分开来却会引起一些很严重的问题,比如我在一个很复杂的条件分支里面进行pulse,但是很不幸,其中一个分支我忘了写pulse,在分支出来之后进行了wait。这样的话就会彻底完蛋,大家都在等待。所以说,肯定有一个什么原因,使得MS决定要把Pulse和Wait分开来,而不是Wait自动发出一个Pulse。这个原因要么因为MS的错误没有在MSDN里面讲清楚,要么因为MS的错误设计成了这个样子。(当然,我想还有第三个原因——为了和java兼容,至于java里面的notify和wait是怎么样运行的,我不清楚,不好评论。)
很可惜的是,Google不到任何一个中文的网页提到这一个问题!基本上就是人云亦云,照抄MSDN上面的例子(不一定是VS的Help,有一些抄的是MS网站上的内容),或者稍微改头换面了一下。
最后提醒大家一下:请不要给我回复和本文第二段相似的内容,请思考思考再思考。
如果你不清楚要点,我最后再总结一下你需要思考的问题:
1、为什么pulse不能够合并到wait里面?
2、pulse之后,wait之前能够强行调度线程吗?
3、pulse之后,wait之前的其他代码有什么意义?
4、pulse的帮助里面那一句话(本文上面红色字体的那句话)到底想告诉我们什么?
5、有没有可能只是pulse不wait?或者不pulse就wait?(实际上第一个进入lock的线程确实可以不pulse)
6、第五点括号中,可以不pulse不代表部能够pulse吗?
7、对于第六点,如果不能够pulse,如果对于你不知道哪一个线程会首先进入lock的情况,你打算怎么设计这个代码?
8、对于第六点,如果能够,那么请回过头来思考第一、二、三、四点。
9、对于第五点,如果你想不出什么答案,请参考第八点。