SUMTEC -- There's a thing in my bloglet.

But it's not only one. It's many. It's the same as other things but it exactly likes nothing else...

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  207 随笔 :: 19 文章 :: 1637 评论 :: 12 Trackbacks

很为中国人伤心,因为这是一片人云亦云的地方。为什么?我问大家一个问题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、对于第五点,如果你想不出什么答案,请参考第八点。

posted on 2004-06-04 10:22 Sumtec 阅读(1864) 评论(8)  编辑 收藏 所属分类: .NET 技术内幕

评论

#1楼  2004-06-04 12:34 Justin Shen      
我做了一些试验,发现msdn并没有错
只有wait()返回之后你才获得了对象的使用权,在你下一次再做wait()之前,你都不会释放对象的使用权。

至于pulse的作用:在调用了wait()之后,线程会进入这个对象的等待队列,而把对象的使用权交给位于这个对象的就绪队列中的第一个线程。如果只有wait()而没有pulse(),那么最终的情况会是所有的线程都处于wait队列而无休止的等待。

在msdn的那个例子中,程序最终的退出是因为SecondThread的wait()函数调用超时。

你可以把SecondThread中的那句while改成:
while(Monitor.Wait(m_smplQueue)
这样你就会发现在打印出所有的数字后,程序死在那里了。这是因为此时FirstThread已经退出了(循环次数已经满),没有人再来Pulse第二个线程,所以它在最后一次调用Wait()释放了对象的使用权后,一直处于等待队列中,没有人把它唤醒到就绪队列的缘故。

至于怎么证明是Wait()释放对象的锁,而不是Pulse(),我做了一个很简单的实验,在SecondTread里,我只让它做一次Pulse(),而不做那个循环,这时你会发现程序进入了无休止的等待中。如果这时你让FirstThread只做一次Wait(m_smplQueue,10000),你会发现在程序等待10秒之后退出了,也就证明FirstThread确实是因为等待超时而退出的。

所以说,SecondThread在做了Pulse()之后,只到在循环处调用了Wait()之后,才释放了对象的使用权。
  回复  引用  查看    

#2楼  2004-06-04 12:44 Justin Shen      
不把Wait()和Pulse()合并的原因,我想是因为这样:

我在得到这个对象的使用权之后,希望此时处于Wait()的线程都不再得到这个对象的使用权,那么我在完成处理之后,不调用Pulse()直接调用Wait(someObject,1);来归还对象的使用权之后,所有此时生于Wait()的线程都不会得到这个对象,便在这之后,如果有其它的线程调用lock(someObject),就应该能获得这个对象的使用权。
  回复  引用  查看    

#3楼  2004-06-04 13:00 Justin Shen      
我写了个小程序来说明上面的问题:
using System;
using System.Threading;
using System.Collections;

namespace MonitorCS1
{
public class MonitorSample
{

object o;

public MonitorSample()
{
o = new object();
}
public void FirstThread()
{
lock(o)
{
Monitor.Wait(o);
Console.WriteLine("in first thread");
//Monitor.Pulse(o);
Monitor.Exit(o);
}

}
public void SecondThread()
{
lock(o)
{
Monitor.Pulse(o);
if(Monitor.Wait(o,5000))
{
Console.WriteLine("in second thread");
}
}

}

public void ThirdThread()
{
Thread.Sleep(2000);
lock(o)
{
Monitor.Enter(o);
Console.WriteLine("in third thread");
Monitor.Exit(o);
}
}
}

class Program
{
static void Main(string[] args)
{

MonitorSample test = new MonitorSample();            

Thread tFirst = new Thread(new ThreadStart(test.FirstThread));
Thread tSecond = new Thread(new ThreadStart(test.SecondThread));
Thread tThird = new Thread(new ThreadStart(test.ThirdThread));

tFirst.Start();
tSecond.Start();
tThird.Start();

tFirst.Join();
tSecond.Join(); 
tThird.Join();


}
}
}

如果FirstThread调用Pulse,SecondThread会得到对象的使用权,如果不调用,直接Exit,那么SecondThread会因为等待超时而退出,但ThirdThread不管怎样都会得到对象的使用权。
  回复  引用  查看    

#4楼  2004-06-04 13:08 Justin Shen      
顺便说一下,如果ThirdThread改成:

public void ThirdThread() 

Thread.Sleep(2000); 
lock(o) 

Monitor.Enter(o); 
Console.WriteLine("in third thread"); 
Monitor.Pulse(o);
Monitor.Exit(o); 


那么SecondThread会最后得到使用权。

  回复  引用  查看    

#5楼  2004-06-05 04:54 saucer [未注册用户]
Justin Shen 的理解是正确的

该synchronized对象持有几个引用,一个是对当前拥有该对象 lock的线程,一个是ready queue,一个是waiting queue

注意,Wait/Pulse是在lock内或Monitor.Enter/Monitor.Exit之间调用的。lock把当前线程加到ready queue里去,直到获取该对象为止。得到对象后,有时当前线程为了与其他线程做协调,可以用Wait暂时放弃该对象的所有权,同时把自己加到waiting queue里去,如果别的线程获取该对象后不调用Pulse的话,当前线程就死在waiting queue里了,除非用Monitor.Wait Method (Object, Int32),在一定时间后,即使获取不了该对象,也自动把自己加到ready queue里去

据说Wait/Pulse/PulseAll 是与JAVA中对wait/notify/notifyAll相仿,参考

http://www.soften.ktu.lt/~mockus/gmcsharp/csharp/c-sharp-vs-java.html
  回复  引用    

那是不是意味着,Monitor.enter是将同步线程放入ReadQueue,而Monitor.wait是将目前资源拥有者,放入WaitQueue并交出锁。而Monitor.Pulse是将WaitQueue的队首,放入ReadQueue的队尾
  回复  引用    

#7楼  2004-10-27 22:50 msolap      
呵呵,sumtec的钻劲实足啊。
其实如果熟悉Windows SDK的话,应该知道Monitor实际上是对Event核心对象的方法的封装。在Platform SDK中查找SetEvent/ResetEvent/PulseEvent就可以找到了答案。
弄明白Event,便是一通百通。

另外,可以看.netfx的source code(search "rotor" in www.microsoft.com),里面总能找到些线索。
  回复  引用  查看    

#8楼  2005-08-09 22:14 飞刀      
本想长篇大论一把,结果发现这贴已经有一年多的历史了:(


  回复  引用  查看    


标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      


相关链接: