如何在 MVC 3 中正确进行长轮询
Posted
技术标签:
【中文标题】如何在 MVC 3 中正确进行长轮询【英文标题】:How to do long polling properly in MVC 3 【发布时间】:2012-06-22 11:07:35 【问题描述】:我正在尝试连接 AsyncController,以便当用户在订单页面上单击保存订单时,查看同一订单的所有用户都应该收到订单已更改的通知。我实现这一点的方法是在订单页面上执行长轮询 ajax 请求,但是如何制作可扩展的 AsyncController 来处理这对我来说并不明显。
这就是我目前所拥有的,ID 是指示已更改或轮询更改的订单的 ID。
public class MessageController : AsyncController
static readonly ConcurrentDictionary<int, AutoResetEvent> Events = new ConcurrentDictionary<int, AutoResetEvent>();
public ActionResult Signal(int id)
AutoResetEvent @event;
if (Events.TryGetValue(id, out @event))
@event.Set();
return Content("Signal");
public void WaitAsync(int id)
Events.TryAdd(id, new AutoResetEvent(false));
// TODO: This "works", but I should probably not block this thread.
Events[id].WaitOne();
public ActionResult WaitCompleted()
return Content("WaitCompleted");
我看过 How to do long-polling AJAX requests in ASP.NET MVC? 。我试图了解有关此代码的所有详细信息,但据我了解此代码它阻塞了线程池中的每个工作线程,据我所知,这最终会导致线程饥饿。
那么,我应该如何以一种很好的、可扩展的方式来实现它?请记住,我不想再使用任何第三方组件,我想很好地了解如何正确实施此方案。
【问题讨论】:
【参考方案1】:实际上我能够在不阻塞工作线程的情况下实现这一点,我缺少的是 ThreadPool.RegisterWaitForSingleObject。
public class ConcurrentLookup<TKey, TValue>
private readonly Dictionary<TKey, List<TValue>> _lookup = new Dictionary<TKey, List<TValue>>();
public void Add(TKey key, TValue value)
lock (_lookup)
if (!_lookup.ContainsKey(key))
_lookup.Add(key, new List<TValue>());
_lookup[key].Add(value);
public List<TValue> Remove(TKey key)
lock (_lookup)
if (!_lookup.ContainsKey(key))
return new List<TValue>();
var values = _lookup[key];
_lookup.Remove(key);
return values;
[SessionState(SessionStateBehavior.Disabled)]
public class MessageController : AsyncController
static readonly ConcurrentLookup<int, ManualResetEvent> Events = new ConcurrentLookup<int, ManualResetEvent>();
public ActionResult Signal(int id)
foreach (var @event in Events.Remove(id))
@event.Set();
return Content("Signal " + id);
public void WaitAsync(int id)
AsyncManager.OutstandingOperations.Increment();
var @event = new ManualResetEvent(false);
Events.Add(id, @event);
RegisteredWaitHandle handle = null;
handle = ThreadPool.RegisterWaitForSingleObject(@event, (state, timeout) =>
handle.Unregister(@event);
@event.Dispose();
AsyncManager.Parameters["id"] = id;
AsyncManager.Parameters["timeout"] = timeout;
AsyncManager.OutstandingOperations.Decrement();
, null, new TimeSpan(0, 2, 0), false);
public ActionResult WaitCompleted(int id, bool timeout)
return Content("WaitCompleted " + id + " " + (timeout? "Timeout" : "Signaled"));
【讨论】:
【参考方案2】:现在使用 async/await 实现长轮询要容易得多。
public class MessageController : ApiController
private static readonly ConcurrentDictionary<int, ManualResetEventAsync> ManualResetEvents = new ConcurrentDictionary<int, ManualResetEventAsync>();
[HttpGet]
public IHttpActionResult Signal(int id)
if (ManualResetEvents.TryGetValue(id, out var manualResetEvent) == false)
return Content(HttpStatusCode.OK, "Signal: No one waiting for signal");
manualResetEvent.Set();
return Content(HttpStatusCode.OK, "Signaled: " + id);
[HttpGet]
public async Task<IHttpActionResult> Wait(int id)
var manualResetEvent = ManualResetEvents.GetOrAdd(id, _ => new ManualResetEventAsync());
var signaled = await manualResetEvent.WaitAsync(TimeSpan.FromSeconds(100));
var disposed = manualResetEvent.DisposeIfNoWaiters();
if (disposed)
ManualResetEvents.TryRemove(id, out var _);
return Content(HttpStatusCode.OK, "Wait: " + (signaled ? "Signaled" : "Timeout") + " " + id);
internal class ManualResetEventAsync
private readonly SemaphoreSlim semaphore = new SemaphoreSlim(0, int.MaxValue);
private int waiters;
public void Set()
semaphore.Release(int.MaxValue);
public async Task<bool> WaitAsync(TimeSpan timeSpan)
lock (semaphore)
waiters++;
var task = await semaphore.WaitAsync(timeSpan);
lock (semaphore)
waiters--;
return task;
public bool DisposeIfNoWaiters()
lock (semaphore)
if (waiters != 0)
return false;
semaphore.Dispose();
return true;
【讨论】:
以上是关于如何在 MVC 3 中正确进行长轮询的主要内容,如果未能解决你的问题,请参考以下文章
在带有 turbolink 的 Rails 中使用 jQuery 进行长轮询
使用 WinAPI 的 InternetReadFile() 进行长轮询