问题
I am trying to wire up an AsyncController so that when a user clicks save on an order on the order page, all users viewing the same order should get a notification that the order has changed. My approach to implement this is to do long polling ajax requests on the order page, however how to make a scalable AsyncController to deal with this is not obvious to me.
So this is what I have so far, the ID is the ID of the order that is signaled as changed or polled for changes.
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");
}
}
I have had a look at How to do long-polling AJAX requests in ASP.NET MVC? . I am trying to understand all details about this code but as far as I understand this code it is blocking each worker thread in the thread pool which, as far as I understand would eventually lead to thread starvation.
So, how should I implement this in a nice, scalable way? Bear in mind that I do not wish to use any more third party components, I want to get a good understanding of how to implement this scenario properly.
回答1:
Actually I was able to implement this without blocking the worker threads, the thing I was missing was 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:
Now days it is much easier to implement long polling with 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;
}
}
}
来源:https://stackoverflow.com/questions/11117421/how-to-do-long-polling-properly-in-mvc-3