为什么我的异步ASP.NET Web API控制器会阻塞主线程?

6
我有一个ASP.NET Web API控制器,我认为它应该是异步操作的。这个控制器设计了第一个请求睡眠20秒钟,但会立即处理任何后续请求。所以我的预期时间线应该是这样的:
1. 发出请求1。 2. 发出请求2。 3. 发出请求3。 4. 请求2返回。 5. 请求3返回。 6. 等待约20秒钟。 7. 请求1返回。
但事实上,没有请求返回直到请求1完成为止。
根据调试输出,我可以确认入口线程和休眠线程的ID是不同的。我故意使用了TaskCreationOptions.LongRunning来将休眠强制放在一个单独的线程中,但是应用程序仍然拒绝服务任何新的请求,直到该休眠完成。
我是否忽略了关于异步Web API控制器工作方式的基本知识?
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);
    }
}

作为第一步,我会将“_firstTime”标记为“volatile”,以确保不同的线程可以一致地看到它的变化。 - Damien_The_Unbeliever
你使用哪个Web服务器进行托管? - Davin Tryon
将“_firstTime”标记为“volatile”没有任何区别。我已经尝试在Win 7 Pro上使用IIS Express 8.0和IIS 7.5进行托管。 - Snixtor
@Snixtor - 这可能没有什么区别,但这是应该做的事情。 - Damien_The_Unbeliever
@Damien_The_Unbeliever 已经注意到了。我承认我不得不查一下它的意思,它绝对与我的示例代码相关。 - Snixtor
Task<string> Get() 方法会调用 Result 或 Wait() 吗?这些都是阻塞操作。 - Alex
2个回答

14

这实际上与服务器无关,而与客户端有关。Chrome和Firefox似乎不想发送他们认为是“重复”的请求,直到第一个请求收到响应。任何浏览器的“私人”会话都可以立即返回第二个请求。Internet Explorer 9似乎没有表现出这种行为。

为了与客户端实现隔离,我编写了以下客户端。

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);
    }
}

它的输出结果如下:

  1. 客户端 2 接收到数据:"FOOBAR"(在启动后的几毫秒内)
  2. 客户端 3 接收到数据:"FOOBAR"
  3. 任务 2 最先完成
  4. (长时间等待)
  5. 客户端 1 接收到数据:"FOOBAR"
  6. 所有任务都已完成

1
以为我疯了。感谢你的启示! - NovaJoe

0

在我的情况下,这是输出(它从5到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).

这可能是由于环境和机器运行(单核)导致运行时决定如何运行它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接