7

I have an ASP.NET Web API controller that I would have thought would operate asynchronously. The controller is designed to sleep 20 seconds for the first request, but service any subsequent requests immediately. So my anticipated timeline would be something like:

  1. Raise request 1.
  2. Raise request 2.
  3. Raise request 3.
  4. Request 2 returns.
  5. Request 3 returns.
  6. Wait ~20 seconds.
  7. Request 1 returns.

Instead, no requests return until request 1 is finished.

I can confirm (based on the debug outputs), that the entry thread and sleepy thread id are different. I've intentionally used TaskCreationOptions.LongRunning to force the sleep onto a separate thread, but still the application refuses to service any new requests until that sleep has finished.

Am I missing something fundamental about how async Web API controllers really work?


public class ValuesController : ApiController
{
    private static bool _firstTime = true;

    public async Task<string> Get()
    {
        Debug.WriteLine("Entry thread id: {0}. Sync: {1}",
            Thread.CurrentThread.ManagedThreadId,
            SynchronizationContext.Current);
        await LongWaitAsync();
        return "FOOBAR";
    }

    private Task LongWaitAsync()
    {
        return Task.Factory.StartNew(() =>
            {
                if (_firstTime)
                {
                    _firstTime = false;
                    Debug.WriteLine("Sleepy thread id: {0}. Sync: {1}",
                        Thread.CurrentThread.ManagedThreadId,
                        SynchronizationContext.Current);
                    Thread.Sleep(20000);
                    Debug.WriteLine("Finished sleeping");
                }
            },
            CancellationToken.None,
            TaskCreationOptions.LongRunning,
            TaskScheduler.Default);
    }
}
6
  • As a first step, I'd mark _firstTime as volatile, to make sure that separate threads can see it changing consistently. Commented Feb 21, 2013 at 7:17
  • What web server are you using to host? Commented Feb 21, 2013 at 7:17
  • Marking _firstTime as volatile makes no difference. I've tried hosting in IIS Express 8.0 and IIS 7.5, both on Win 7 Pro. Commented Feb 21, 2013 at 7:20
  • @Snixtor - it may make no difference, but it's something that ought to be done anyway. Commented Feb 21, 2013 at 7:20
  • @Damien_The_Unbeliever Duly noted. I'll admit I had to look up its meaning and it's definitely relevant to my example code. Commented Feb 21, 2013 at 7:22

2 Answers 2

14

This actually has nothing to do with the server, and everything to do with the client. Both Chrome and Firefox don't appear to want to send what they deem a "duplicate" request until the first one has its response. A separate "private" session of either browser will return from the second request immediately. Internet Explorer 9 doesn't seem to exhibit this behaviour.

To isolate from client implementations, I put together the following client.

class Program
{
    static void Main(string[] args)
    {
        var t1 = Task.Run(() => FetchData(1));
        var t2 = Task.Run(() => FetchData(2));
        var t3 = Task.Run(() => FetchData(3));
        
        var index = Task.WaitAny(t1, t2, t3);
        Console.WriteLine("Task {0} finished first", index + 1);

        Task.WaitAll(t1, t2, t3);
        Console.WriteLine("All tasks have finished");

        Console.WriteLine("Press any key");
        Console.ReadKey(true);
    }

    static void FetchData(int clientNumber)
    {
        var client = new WebClient();
        string data = client.DownloadString("http://localhost:61852/api/values");
        Console.WriteLine("Client {0} got data: {1}", clientNumber, data);
    }
}

Its output goes:

  1. Client 2 got data: "FOOBAR" (within milliseconds of startup)
  2. Client 3 got data: "FOOBAR"
  3. Task 2 finished first
  4. (long wait here)
  5. Client 1 got data: "FOOBAR"
  6. All tasks have finished
Sign up to request clarification or add additional context in comments.

1 Comment

Thought I was going crazy. Thanks for the revelation!
0

In my case here is output (it switches to another thread from 5 to 10):

Entry thread id: 5. Sync: System.Web.LegacyAspNetSynchronizationContext
Sleepy thread id: 10. Sync: 
Finished sleeping
The thread '<No Name>' (0x2c84) has exited with code 0 (0x0).
Entry thread id: 7. Sync: System.Web.LegacyAspNetSynchronizationContext
The thread '<No Name>' (0x1818) has exited with code 0 (0x0).
Entry thread id: 5. Sync: System.Web.LegacyAspNetSynchronizationContext
The thread '<No Name>' (0xd4c) has exited with code 0 (0x0).
The thread '<No Name>' (0x2c30) has exited with code 0 (0x0).

This could be due to environment and machine running (single core) that makes the runtime decide how to run it.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.