At work I am responsible for a program which involves a very large number of authenticated HTTP requests to retrieve data. In an effort to make it as efficient as possible, I used asynchronous HTTP requests. Furthermore, I needed to retrieve and cache certain data from the webserver across threads.
However, I ran across a terrible problem. To illustrate, I’ve simplified the code down to the following (the TimedLock object is from my post Useful IDisposable Class 1: TimedLock (Post 3 of 5)):
class Class1
{
private ValueType sharedData;
private object sharedDataLock = new object();
public void Run()
{
for (int i = 0; i < 100; i++)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(Process));
}
}
private void Process(object state)
{
using (TimedLock.TryLock(sharedDataLock, TimeSpan.FromSeconds(60)))
{
WebRequest request = HttpWebRequest.Create(...);
using (WebResponse response = request.GetResponse())
{
// update sharedData
}
}
// Do other work
}
}
The problem manifests itself as a lock timeout (or a deadlock if you use the lock keyword): One thread acquires the lock in TimedLock.TryLock() but request.GetResponse() blocks forever, so it never releases the lock. The other threads remain stuck at the TimedLock.TryLock() line, waiting for the first thread to relinquish the lock.
Why does this happen? Well, it took a long time for me to figure out, but I finally determined it is due to the problem described in this .NET Matters article:
The first thing to be aware of is that in version 1.x of the Microsoft®.NET Framework, HttpWebRequest never makes synchronous requests. What do I mean by that? Take a look at the code for HttpWebRequest.GetResponse as coded in the Shared Source CLI (SSCLI), shown here omitting the code that checks to see if the response was previously retrieved and that accounts for timeouts:
public override WebResponse GetResponse() {
•••
IAsyncResult asyncResult = BeginGetResponse(null, null);
•••
return EndGetResponse(asyncResult);
}
As you can see, HttpWebRequest.GetResponse is simply a wrapper around the pairing of BeginGetResponse and EndGetResponse. These operate asynchronously, meaning that BeginGetResponse makes the actual HTTP request from a different thread than the one from which it was called, and EndGetResponse blocks until the request has completed. The net result of this is that HttpWebRequest queues a work item to the ThreadPool for every outbound request.
In the scenario I have provided, the ThreadPool spins up a number of threads to process the QueueUserWorkItem() requests, which then all block on TimedLock.TryLock() — except for one, which gets to request.GetResponse(). Then, when GetResponse() attempts to grab a ThreadPool thread of its own (per the article), it deadlocks waiting for a ThreadPool thread to become free. Incidentally, the HttpWebRequest class is supposed to throw an Exception if the number of threads in the ThreadPool is too low, but that didn’t seem to be happening for me.
What’s the solution? I first tried keeping a minimum number of ThreadPool threads available by explicitly checking the number of available threads in the ThreadPool by using ThreadPool.GetAvailableThreads() before calling QueueUserWorkItem(), but ThreadPool threads aren’t started immediately — they are started up at a later time, spaced apart with a small delay — so GetAvailableThreads() indicated that there were plenty of threads available.
Outside of upgrading to the .NET Framework 2.0 — which isn’t even released yet — the article suggests writing a “throttling” ThreadPool which handles thread management itself and limits the number of active threads to a programmer-specified maximum number. Here’s the article’s sample implementation:
public sealed class Semaphore : WaitHandle
{
public Semaphore() : this(1, 1) {}
public Semaphore(int initialCount, int maximumCount)
{
if (initialCount < 0 || initialCount > maximumCount)
throw new ArgumentOutOfRangeException("initialCount");
if (maximumCount < 1)
throw new ArgumentOutOfRangeException("maximumCount");
IntPtr h = CreateSemaphore(
IntPtr.Zero, initialCount, maximumCount, null);
if (h == WaitHandle.InvalidHandle || h == IntPtr.Zero)
throw new Win32Exception();
Handle = h;
}
public void ReleaseOne()
{
int previousCount;
if (!ReleaseSemaphore(Handle, 1, out previousCount))
throw new Win32Exception();
}
[DllImport("kernel32.dll", SetLastError=true)]
private static extern IntPtr CreateSemaphore(
IntPtr lpSemaphoreAttributes, int lInitialCount,
int lMaximumCount, string lpName);
[DllImport("kernel32.dll", SetLastError=true)]
private static extern bool ReleaseSemaphore(
IntPtr hSemaphore, int lReleaseCount, out int lpPreviousCount);
}
public class ThreadPoolThrottle : IDisposable
{
private Semaphore _throttle;
public ThreadPoolThrottle(int maximumAllowed)
{
if (maximumAllowed > 1)
throw new ArgumentOutOfRangeException("maximumAllowed");
_throttle = new Semaphore(maximumAllowed,maximumAllowed);
}
public void QueueUserWorkItem(WaitCallback callback)
{
QueueUserWorkItem(callback, null);
}
public void QueueUserWorkItem(WaitCallback callback, object state)
{
if (_throttle == null)
throw new ObjectDisposedException(this.GetType().FullName);
if (callback == null)
throw new ArgumentNullException("callback");
_throttle.WaitOne();
try
{
QueuedCallback qc = new QueuedCallback();
qc.Callback = callback;
qc.State = state;
ThreadPool.QueueUserWorkItem(
new WaitCallback(HandleWorkItem), qc);
}
catch
{
_throttle.ReleaseOne();
throw;
}
}
private void HandleWorkItem(object state)
{
QueuedCallback qc = (QueuedCallback)state;
try { qc.Callback(qc.State); }
finally { _throttle.ReleaseOne(); }
}
private class QueuedCallback
{
public WaitCallback Callback;
public object State;
}
public void Dispose()
{
if (_throttle != null)
{
((IDisposable)_throttle).Dispose();
_throttle = null;
}
}
}
I was able to easily adapt this solution for my needs. Unfortunately, I do not have a solution if one desires to continue using asynchronous method calls — such as delegates’ BeginInvoke() method or Stream.BeginRead() — as they internally use ThreadPool threads.
Recent Comments