I have an ASP.NET app, very basic, but right now too much code to post if we’re lucky and I don’t have to.
We have a class called ReportGenerator. On a button click, method GenerateReports is called. It makes an async call to InternalGenerateReports using ThreadPool.QueueUserWorkItem and returns, ending the ASP.NET response. It doesn’t provide any completion callback or anything.
InternalGenerateReports creates and maintains five threads in the threadpool, one report per thread, also using QueueUserWorkItem, by ‘creating’ five threads, also with and waiting until calls on all of them complete, in a loop. Each thread uses an ASP.NET ReportViewer control to render a report to HTML. That is, for 200 reports, InternalGenerateReports should create 5 threads 40 times. As threads complete, report data is queued, and when all five have completed, report data is flushed to disk.
My biggest problems are that after running for just one report, the aspnet process is ‘hung’, and also that at around 200 reports, the app just hangs.
I just simplified this code to run in a single thread, and this works fine. Before we get into details like my code, is there anything obvious in the above scendario that might be wrong?
Here is an abreviated example of the code:
public class SscceReports
{
Dictionary<Guid, AsyncReportCreator> runningWorkers = new Dictionary<Guid, AsyncReportCreator>();
public void GenerateReports(Queue<int> reportKeys)
{
int goodPoolSize = System.Environment.ProcessorCount;
System.Threading.ThreadPool.SetMaxThreads(goodPoolSize + 10, goodPoolSize * 10);
System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(InternalGenerateReports), reportKeys);
}
void InternalGenerateReports(object state)
{
Queue<int> reportKeys = state as Queue<int>;
while (reportKeys.Count > 0)
{
for (int i = 0; i < 5 && reportKeys.Count > 0; i++)
{
Guid workerId = Guid.NewGuid();
int rk = (int) reportKeys.Dequeue();
AsyncReportCreator asrc = new AsyncReportCreator(rk);
runningWorkers.Add(workerId, asrc);
asrc.WorkComplete += CompleteCallBack;
System.Threading.ThreadPool.QueueUserWorkItem(asrc.StartWork);
}
while (runningWorkers.Count > 0)
System.Threading.Thread.Sleep(500);
}
while (runningWorkers.Count > 0)
System.Threading.Thread.Sleep(5000);
}
void CompleteCallBack(object state)
{
// Write queued report content to disk.
runningWorkers.Remove((Guid) state);
}
}
public class AsyncReportCreator
{
public event System.Threading.WaitCallback WorkComplete;
private int key;
public AsyncReportCreator(int reportKey)
{
key = reportKey;
}
public void StartWork(object state)
{
// Create report;
WorkComplete(state);
}
}
The .NET
ThreadPoolby default has 25 threads per processor, so if you’re generating 200 reports when you callInternalGenerateReportsit will queue 1,000 work items in theThreadPool(200 reports * 5 work items for eachInternalGenerateReports). Only 25 will be active at a time, but you can see that this model is probably not suitable for you.Think about using a producer/consumer pattern and instead of queuing 25 work items (i.e 25 threads), just create a few consumer threads (matching the number of processors/cores you have) which process requests for report generation from a central blocking queue which is populated by the producer(s). That should make things a bit more reasonable…
There are some articles that say you shouldn’t use a
ThreadPoolwith ASP.NET:Update
I think I found your issue… you’re queuing
asrc.StartWorkbut you’re not passing in astateso whenCompleteCallBackis called it doesn’t have anything to remove from therunningWorkers. Here is an improved version for you: