作者:陈广琛
在上一次的文章中,我们说到了如何设计一个 ASP.NET Web Service来处理长连接请求。很多人对此就提出了问题,如何 hold住请求让它 30秒不断开了?这其实很简单,只需要 Sleep()一下就可以了:
Thread.Sleep(30 * 1000);
然而问题是,我们不是要等 30秒然后看看是否有事件需要返回,而是在这 30秒内随时有事件随时返回。因此,我们需要一套机制来在等待的过程中检查是否有事件发生了。
Monitor模型
在 .NET里面,大家最熟悉的线程同步模型应该就是 Monitor模型了。没听说过?就是 C#的那个 lock关键字,实际上它编译出来就是一对 Monitor.Enter()和 Monitor.Exit()。
通过 lock命令,我们可以针对一个对象创建一个临界区,代码执行到临界区入口时必须获取到该对象的锁才能执行下去,并且在临界区的出口释放该锁。然而这种模型不太适用于解决我们的问题,因为我们需要等待一个事件,如果使用 lock来等待的话,那就是说要先在 We b Service外部把对象锁上,然后等事件触发了就解锁,这时候 Web Service才顺利进入临界区域。
事实上,要进行这类型的阻塞,还有一个更好的选择,那就是 Mutex。
Mutex模型
Mutex,也就是 mutual exclusive的缩写, “互斥 ”的意思。 Mutex是如何运作的?这有点像是银行的排队叫号系统,所有等待服务的人都坐在大厅里等候( wait)被叫,当一个服务窗口空闲时它就会发出一个信号( signal)来通知下一位等候服务的人。总之,所有执行 wait指令的线程都在等候,而每一个 signal能够让一个线程结束等候继续执行。
在 .NET里面, wait和 signal这两个操作分别对应 Mutex.WaitOne()和 Mutex.ReleaseMutex()这两个方法。我们可以让 Web Service的线程使用 Mutex.WaitOne()进入等候状态,而在事件发生时使用 Mutex.ReleaseMutex()来通知 Web Service线程。因为必须在 Mutex.ReleaseMutex()发生后 Mutex.WaitOne()才可能继续执行下去,因此能够执行下去就证明必然有事件发生了并且调用了 Mut ex.ReleaseMutext(),这时候就可以放心地去读取事件消息了。
简单示例
在选定使用 Mutex模型后,我们来编写一个简单的示例。首先,我们要在 WebService派生类内定义一个 Mutex,还有一个代表消息的字符串。
Mutex mutex = new Mutex();
string message;
然后,我们定义两个 WebMethod。为了把问题简单化,我们选用上一篇文章中开头所说的两个函数签名,也就说只能在一个 Web Service内自己发自己收,没有发送目标的概念,也没有超时的概念,还没有可靠性设计。同时,我们将 Message类型替换为普通字符串,以便于我们测试。
我们先编写发送消息的函数:
public void Send(string message) {
this.message = message;
this.mutex.ReleaseMutex();
}
在这个发送函数里,首先我们把消息放进了类内全局的变量中,然后让全局的 Mutex类释放一个 signal。这时候,如果有线程在等待,它可以马上执行下去。如果此时没有线程在等待,那么下一个 wait的线程执行到该阻塞的地方就能够不受阻塞继续执行下去。
现在我们来编写接收消息的函数:
public string Wait() {
this.mutex.WaitOne();
return this.message;
}
接收函数一开始就进入 wait状态。在得到 signal后,需要做的事情就是把全局的消息返回给客户端。
亲身体验
最后,我们可以通过 ASP.NET Web Service本身支持的 Web测试界面来测试一下我们的代码。我们开两个浏览器窗口,一个进入 Send()调用,一个进入 Wait()调用。然后我们按照如下方法来测试:
1. 首先执行 Send("Hello"),然后执行 Wait()。这时候你可以马上看到 "Hello"。
2. 首先执行 Wait(),让它等待返回,这时候执行 Send("Hello")。随后你可以看到 Wait()那段返回 "Hello"了。
3. 按如下顺序执行: Send("Hello");Wait(); Send("World");Wait();
4. 按如下顺序执行: Send("Hello");Send("World");Wait();Wait();
5. 按如下顺序执行: Wait();Wait();Send("Hello");Send("World");
6. 按如下顺序执行: Wait();Send("Hello");Wait();Send("World");
你会发现这样一些奇怪的结果:第 3个测试返回的是 "World"和 "World"。第 5个测试先返回 "Hello"的并不一定是先执行的那个 Wait()线程。后者在某些情况下不是什么问题,特别是长连接中一般之后一个 Wait()线程在等待中,所以我们可以不管。而前者,则是因为没有消息队列所造成的,我们只有长度为 1的消息窗口,所以只能缓存最后一个消息。这个问题我们将在下一篇文章中解决。
小结
在本文中,我们看到了不同的线程同步模型的差异。 Monitor模型的 lock本质上是一个 Semaphore,也就是一个不能连续 signal的 Mutex,一个 signal发出去后必须被一个 wait接收了才能进行下一次的 signal。同时, Semaphore也限制了 signal和 wait必须在同一个线程内成对执行,而 Mutex则没有此限制。虽然 .NET是针对 Monitor模型优化的,但在我们的需求当中,只能通过 Mutex模型来解决。
接着,我们便写了一个小小的消协发送与接收函数,实现了我们想要的阻塞式 Web Service。同时我们也看到了没有消息队列造成的问题,因此确定接下来我们要做一个消息队列。