1

I'm developing an ASP.Net application that is doing some heavy async works and informing the client-side instantly by some javascript codes during the work.

During my tests, I've realized it's working very good until I've discovered something else. When that single async page is opened and started to run, all other future client requests are blocked (held) by the IIS until the async page completes the work. IIS accepts the connection (I guess only to prevent connection timeouts on the browser side) but sends absolutely nothing until the async page completely finishes. When the async job completes, it resumes the process future request and everything goes back to normal.

But, of course, this is problematic. I honestly did not expect this coming since this is why threads are being used, to not to block the UI, right?

Here is the sample code I've developed for you to test who is interested in the topic. You can create new project or even add it to one of the existing asp.net application. After doing so, just go to /async.aspx and when it started to counts asynchronously, just open up another tabpage on the browser and start requesting some other subpages on the same domain. You will experience that eventually the website stops responding to future request until async work is finished.

I say 'eventually' because for this simple example code sometimes IIS responds for the first one to three requests and then goes "on hold" state.

BUT, in my main project, the async work is much heavier than counting on 2 threads in the sample code, therefore it gets impossible to load even the very first page when my job is started.

async.aspx:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="async.aspx.cs" Inherits="async.test.app.async" Async="true" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Async Test</title>
    <script>
        function updatePercent(id, p) {
            document.getElementById(id).innerText = p;
        }
        function allDone() {
            var labels = document.getElementsByClassName("percent");
            for (var i = 0; i < labels.length; i++)
                labels[i].innerText = "Completed!";
        }
    </script>
</head>
<body>
    <form id="form1" runat="server">
        <h1>Async test page</h1>
        <div>Server-side async task #1 percent is: <span id="percent1" class="percent"></span></div>
        <div>Server-side async task #2 percent is: <span id="percent2" class="percent"></span></div>
    </form>
</body>
</html>
<%
    //calling to start right here makes it sure to browser load up the content so far - using response.flush in StartAsyncWork()
    StartAsyncWork();
%>

async.aspx.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Threading;

namespace async.test.app
{
    public partial class async : System.Web.UI.Page
    {

        ManualResetEvent[] MRE;
        object response_locker;

        protected void Page_Load(object sender, EventArgs e)
        {

        }

        public void StartAsyncWork()
        {

            //flush buffer first
            Response.Flush();

            response_locker = new object();

            //create first task (count to 20)
            ManualResetEvent mre1 = new ManualResetEvent(false);
            Thread Thread1 = new Thread(() => CountTo(mre1, "percent1", 20));

            //create second task (count to 30)
            ManualResetEvent mre2 = new ManualResetEvent(false);
            Thread Thread2 = new Thread(() => CountTo(mre2, "percent2", 30));

            //prepare waithandles
            MRE = new ManualResetEvent[] { mre1, mre2 };

            //start tasks
            Thread1.Start();
            Thread2.Start();

            //wait for all tasks to complete
            ManualResetEvent.WaitAll(MRE);

            Response.Write("<script>allDone();</script>");
            Response.Flush();

        }

        protected void CountTo(ManualResetEvent mre, string id, int countTo) //counts to 60
        {
            try
            {
                for (int i = 1; i <= countTo; i++)
                {
                    lock (response_locker)
                    {
                        Response.Write(string.Format("<script>updatePercent('{0}','{1}/{2}')</script>", id, i, countTo));
                        Response.Flush();
                    }
                    Thread.Sleep(1000);
                }

            }
            catch (Exception)
            {
                throw;
            }
            finally
            {
                mre.Set();
            }
        }

    }
}
Roni Tovi
  • 828
  • 10
  • 21
  • Are you doing actual async work, i.e. IO or CPU bound work? Based on your example, you're not doing actual async stuff. – JohanP May 20 '19 at 07:09
  • This is just a simpliest example to create the issue. In my main app, I'm actually doing async works - my page uses an event-driven librarly of my own. However, the result is all the same. CPU bound work or real async work - it seems it doesn't matter of IIS behaviour. As long as that page is working, IIS locks the application pool just as the same. – Roni Tovi May 20 '19 at 07:15
  • @RoniTovi "why threads are being used, to not to block the UI?" - But you're blocking right here: `ManualResetEvent.WaitAll(MRE);` – Ilian May 20 '19 at 07:45
  • @Ilian Yes but I would expect to block the current Request only, not the whole application pool and future requests. This is how to wait for the threads to complete. Besides, you need to WAIT somewhere otherwise the page will immediately ends when it reaches the end of the code even the page is set to Async. – Roni Tovi May 20 '19 at 07:50
  • You have a default of processor count threads in your threadpool. So lets say you have 4 processors, that means you start off with 4 threads to serve your requests. You will get more threads injected at a rate of 2 a second. If you are using 2 threads in your code, that means 3 threads are getting used. So if you get 2 more requests, that means 1 will block. You can imagine how quickly this adds up. In your library code, how are you doing the async work? Are you doing `new Thread()`? – JohanP May 20 '19 at 08:03
  • Yes, I am starting my library methods by `new Thread()` and besides, my library creates other threads and they also create other new threads. It is a full multi-threaded library. It must be this way otherwise the process would take too long to complete. But at the end, each thread signals the parent thread and eventually the main thread gets the final signal. – Roni Tovi May 20 '19 at 09:00
  • I just don't understand how a single request blocks the future requests. I am aware that this is about IIS and application pool but don't application pools run also multi-threaded ? Let's forget about the async pages, think this is a normal asp.net website. What if I would have 200+ requests to my website in a few seconds? And what if 2000+ requests ? Would each request wait for the previous one to complete? This sounds so weird. – Roni Tovi May 20 '19 at 09:08
  • It turns out that my further research on the right topic has come to a solution. `https://stackoverflow.com/questions/4571118/how-to-increase-thread-pool-threads-on-iis-7-0` I managed to keep the IIS responding my future requests by adjusting some settings on the configuration described in the link. Currently I just need to run more tests to make sure if it is actually stabil. I will update my question accordingly when I'm done. – Roni Tovi May 20 '19 at 09:31
  • You've just shoved the problem around by increasing the count of the threadpool, and you're still risking threadpool starvation. I've very rarely needed to increase the size of the threadpool, and every time, it's been the wrong decision. Either you are CPU bound, in which case, the work has no place on a webserver... or you are IO bound, in which case, your understanding of asynchrony needs an overhaul. I've serviced several thousands of **concurrent** connections in IIS without touching threadpool settings... – spender May 20 '19 at 11:20
  • ...and without the threadpool needing to add more threads either. – spender May 20 '19 at 11:22
  • My async page uses my own event-driven class library to get data from external websites, extract the file links and download them and save to file system and register them on the database. There are 5 different websites to check and lots of new threads fire in almost each one to search/extract/download/save/record to db for different datetime intervals asynchronously. So, the threads that are in-directly fired from my async page could reach up to hundreds. Also, considering my web application serves to multiple clients (tens of today maybe will reach hundreds in future) from the same domain... – Roni Tovi May 20 '19 at 14:50
  • means there could be tens maybe more of async.aspx running at the same time. And when that happens I believe the total internal threads could reach to thousands, maybe tens of thousands as I get more and more clients. I am truely aware that at some point I will have to have 2nd,3rd,4th... servers but my expactation would be to host at least a hundred clients on the same machine - with a 8-core CPU. Maybe even more with 32-core CPUs. BUT... My current problem is, I can not host even one client without adding more threads to IIS pool. So it seems like my best option for now. – Roni Tovi May 20 '19 at 14:50

0 Answers0