ChatGPT解决这个技术问题 Extra ChatGPT

ASP.NET MVC 中的异步操作使用 .NET 4 上的 ThreadPool 中的线程

在这个问题之后,我在 ASP.NET MVC 中使用异步操作时感到很舒服。因此,我为此写了两篇博文:My Take on Task-based Asynchronous Programming in C# 5.0 和 ASP.NET MVC Web Applications Asynchronous Database Calls With Task-based Asynchronous Programming Model (TAP) in ASP.NET MVC 4

我对 ASP.NET MVC 上的异步操作有太多的误解。

我总是听到这句话:如果操作异步运行,应用程序可以更好地扩展

我也经常听到这样的句子:如果你有大量的流量,你最好不要异步执行你的查询——消耗 2 个额外的线程来服务一个请求会占用其他传入请求的资源。

我认为这两句话是不一致的。

我没有太多关于线程池如何在 ASP.NET 上工作的信息,但我知道线程池的线程大小是有限的。所以,第二句话必须与这个问题有关。

而且我想知道 ASP.NET MVC 中的异步操作是否使用 .NET 4 上的 ThreadPool 中的线程?

例如,当我们实现 AsyncController 时,应用程序是如何构建的?如果我的流量很大,实现 AsyncController 是个好主意吗?

有没有人可以把我眼前的这个黑色窗帘拿开,并向我解释关于 ASP.NET MVC 3 (NET 4) 上的异步处理?

编辑:

我已将以下文档阅读了近数百次,并且我了解主要交易,但我仍然感到困惑,因为那里有太多不一致的评论。

Using an Asynchronous Controller in ASP.NET MVC

编辑:

假设我有如下控制器操作(虽然不是 AsyncController 的实现):

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}

正如你在这里看到的,我启动了一个操作并忘记了它。然后,我没有等待它完成就立即返回。

在这种情况下,这是否必须使用线程池中的线程?如果是这样,在它完成后,该线程会发生什么? GC 是否在完成后立即进入并清理?

编辑:

对于@Darin 的回答,这是一个与数据库对话的异步代码示例:

public class FooController : AsyncController {

    //EF 4.2 DbContext instance
    MyContext _context = new MyContext();

    public void IndexAsync() { 

        AsyncManager.OutstandingOperations.Increment(3);

        Task<IEnumerable<Foo>>.Factory.StartNew(() => { 

           return 
                _context.Foos;
        }).ContinueWith(t => {

            AsyncManager.Parameters["foos"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });

        Task<IEnumerable<Bars>>.Factory.StartNew(() => { 

           return 
                _context.Bars;
        }).ContinueWith(t => {

            AsyncManager.Parameters["bars"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });

        Task<IEnumerable<FooBar>>.Factory.StartNew(() => { 

           return 
                _context.FooBars;
        }).ContinueWith(t => {

            AsyncManager.Parameters["foobars"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });
    }

    public ViewResult IndexCompleted(
        IEnumerable<Foo> foos, 
        IEnumerable<Bar> bars,
        IEnumerable<FooBar> foobars) {

        //Do the regular stuff and return

    }
}
不确定答案,但值得注意的是异步和多线程是不同的东西。因此,可以有固定数量的异步处理线程。会发生的情况是,当一个页面必须阻塞例如 I/O 时,另一个页面将有机会在同一个线程上运行。这就是这两种说法都成立的原因,异步可以使事情变得更快,但线程过多是个问题。
@ChrisChilvers 是的,异步操作并不总是需要多线程。我已经想到了,但据我所知,我认为我没有控制权。从我的角度来看,AsyncController 启动了它想要多少个线程,但也不确定。在 WPF 等桌面应用程序上是否也有线程池的概念?我认为线程数不是这类应用程序的问题。
我认为问题(因此不一致)是第二条语句在意味着许多线程时使用异步。这可能是因为这就是 asp.net 实现异步页面的方式,因此具体实现混淆了问题(因为导致问题的功能名称将是异步页面),但我不确定具体的实现.所以要么他们的意思是“许多线程”,要么他们的意思是“asp.net X 版本中的异步页面”,因为未来的版本可能会改变实现。或者它们只是意味着使用线程池在页面内执行异步。
@ChrisChilvers 哦,伙计!在这些评论之后我更加困惑:s

j
jao

这是一个excellent article,我建议您阅读以更好地理解 ASP.NET 中的异步处理(这是异步控制器基本上代表的内容)。

让我们首先考虑一个标准的同步动作:

public ActionResult Index()
{
    // some processing
    return View();
}

当对该操作发出请求时,将从线程池中提取一个线程,并在该线程上执行该操作的主体。因此,如果此操作中的处理速度很慢,则您将阻塞该线程以进行整个处理,因此该线程无法重用于处理其他请求。在请求执行结束时,线程返回到线程池。

现在让我们以异步模式为例:

public void IndexAsync()
{
    // perform some processing
}

public ActionResult IndexCompleted(object result)
{
    return View();
}

当向 Index 操作发送请求时,将从线程池中抽取一个线程并执行 IndexAsync 方法的主体。一旦这个方法的主体完成执行,线程就会返回到线程池。然后,使用标准 AsyncManager.OutstandingOperations,一旦您发出异步操作完成的信号,就会从线程池中提取另一个线程,并在其上执行 IndexCompleted 操作的主体,并将结果呈现给客户端。

所以我们在这个模式中可以看到,一个客户端 HTTP 请求可以由两个不同的线程执行。

现在有趣的部分发生在 IndexAsync 方法中。如果您在其中有一个阻塞操作,那么您完全浪费了异步控制器的全部目的,因为您阻塞了工作线程(请记住,此操作的主体是在从线程池中提取的线程上执行的)。

那么我们什么时候才能真正利用异步控制器,你可能会问呢?

恕我直言,当我们进行 I/O 密集型操作(例如对远程服务的数据库和网络调用)时,我们可以获得最大收益。如果你有一个 CPU 密集型操作,异步操作不会给你带来太多好处。

那么为什么我们可以从 I/O 密集型操作中获益呢?因为我们可以使用 I/O Completion Ports。 IOCP 非常强大,因为您在整个操作的执行过程中不会消耗服务器上的任何线程或资源。

它们是如何工作的?

假设我们要使用 WebClient.DownloadStringAsync 方法下载远程网页的内容。您调用此方法将在操作系统中注册一个 IOCP 并立即返回。在整个请求的处理过程中,您的服务器上不会消耗任何线程。一切都发生在远程服务器上。这可能需要很多时间,但您并不在意,因为您不会危及您的工作线程。一旦收到响应,就会发出 IOCP 信号,从线程池中提取一个线程并在该线程上执行回调。但是如你所见,在整个过程中,我们并没有独占任何线程。

FileStream.BeginRead、SqlCommand.BeginExecute 等方法也是如此。

并行化多个数据库调用怎么样?假设您有一个同步控制器操作,其中您按顺序执行了 4 个阻塞数据库调用。很容易计算,如果每个数据库调用需要 200 毫秒,那么您的控制器操作将需要大约 800 毫秒来执行。

如果您不需要按顺序运行这些调用,并行化它们会提高性能吗?

这是个大问题,不容易回答。可能是,可能不是。这完全取决于您如何实现这些数据库调用。如果您使用前面讨论的异步控制器和 I/O 完成端口,您将提高此控制器操作和其他操作的性能,因为您不会垄断工作线程。

另一方面,如果您执行得不好(在线程池中的线程上执行阻塞数据库调用),您基本上会将执行此操作的总时间降低到大约 200 毫秒,但您会消耗 4 个工作线程,所以您可能已经降低了其他请求的性能,这些请求可能由于池中缺少处理它们的线程而变得饥饿。

所以这是非常困难的,如果您还没有准备好对您的应用程序执行广泛的测试,请不要实现异步控制器,因为您可能会弊大于利。仅当您有理由这样做时才实施它们:例如,您已经确定标准同步控制器操作是您的应用程序的瓶颈(当然,在执行广泛的负载测试和测量之后)。

现在让我们考虑您的示例:

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}

当收到对 Index 操作的请求时,会从线程池中抽取一个线程来执行其主体,但其主体仅使用 TPL 调度一个新任务。所以动作执行结束,线程返回线程池。除此之外,TPL 使用线程池中的线程来执行它们的处理。所以即使原来的线程被返回到线程池中,你还是从这个池中抽取了另一个线程来执行任务的主体。因此,您从宝贵的池中危及了 2 个线程。

现在让我们考虑以下内容:

public ViewResult Index() { 

    new Thread(() => { 
        //Do an advanced looging here which takes a while
    }).Start();

    return View();
}

在这种情况下,我们手动生成一个线程。在这种情况下,Index 操作主体的执行可能需要稍长的时间(因为产生一个新线程比从现有池中提取一个线程更昂贵)。但是高级日志记录操作的执行将在不属于池的线程上完成。因此,我们不会危及池中的线程,这些线程仍然可以为其他请求提供服务。


真的很详细,谢谢!假设,我们有 4 个异步任务 (System.Threading.Task) 在 IndexAsync 方法中运行。在这些操作中,我们正在对服务器进行 db 调用。所以,它们都是 I/O 密集型操作,对吧?在这种情况下,我们是创建 4 个单独的线程(还是从线程池中获取 4 个单独的线程)?假设我有一台多核机器,它们也将并行运行,对吗?
@tugberk,数据库调用是 I/O 操作,但这完全取决于您如何实现它们。如果您使用诸如 SqlCommand.ExecuteReader 之类的阻塞数据库调用,那么您将浪费一切,因为这是一个阻塞调用。您正在阻塞执行此调用的线程,如果该线程恰好是池中的线程,那将非常糟糕。只有使用 I/O 完成端口:SqlCommand.BeginExecuteReader,您才会受益。如果您无论做什么都不使用 IOCP,请不要使用异步控制器,因为这对应用程序的整体性能造成的损害大于好处。
好吧,大多数时候我首先使用 EF 代码。我不确定它是否合适。我放了一个样本,展示了我通常做什么。我更新了问题,你能看看吗?
@tugberk,您正在并行运行它们,因此与按顺序运行它们相比,执行的总时间更少。但是为了运行它们,您需要使用工作线程。好吧,实际上 EF 是懒惰的,所以当您执行 _context.Foo 时,您实际上并没有执行任何操作。您只是在构建一个表达式树。对此要特别小心。仅当您开始枚举结果集时才会延迟查询执行。如果这种情况发生在视图中,这可能会对性能造成灾难性影响。要急切地执行 EF 查询,请在末尾附加 .ToList()
@tugberk,您将需要一个负载测试工具,以便在您的网站上并行模拟多个用户,以查看它在重负载下的行为。 Mini Profiler 无法模拟您网站上的负载。它可能会帮助您查看和优化您的 ADO.NET 查询并分析单个请求,当您需要查看您的站点在许多用户点击它的真实世界情况下的行为时,这将是无用的。
K
K. Bob

是的 - 所有线程都来自线程池。您的 MVC 应用程序已经是多线程的,当请求进入时,将从池中取出一个新线程并用于为请求提供服务。该线程将被“锁定”(来自其他请求),直到请求得到完全服务并完成。如果池中没有可用的线程,则请求将不得不等到有可用的线程。

如果您有异步控制器,它们仍然从池中获取一个线程,但是在为请求提供服务时,它们可以放弃线程,同时等待某些事情发生(并且可以将该线程提供给另一个请求)以及原始请求需要线程时它再次从池中得到一个。

不同之处在于,如果您有很多长时间运行的请求(线程正在等待某物的响应),您可能会用完池中的线程来为基本请求提供服务。如果你有异步控制器,你就没有更多的线程了,但是那些正在等待的线程会返回到池中并且可以为其他请求提供服务。

一个近乎真实的例子......想象一下,就像上公共汽车,有五个人在等上车,第一个上车,付款并坐下(司机满足了他们的要求),你上车(司机正在服务您的请求)但您找不到您的钱;当您在口袋里摸索时,司机放弃了您并让接下来的两个人上车(满足他们的要求),当您找到您的钱时,司机再次开始与您打交道(完成您的请求)-第五个人必须等到你已经完成了,但第三和第四个人得到了服务,而你已经完成了一半的服务。这意味着驱动程序是池中唯一的线程,乘客是请求。如果有两个驱动程序,写它会如何工作太复杂了,但你可以想象......

如果没有异步控制器,您身后的乘客将不得不等待很长时间才能找到您的钱,同时公交车司机将无所事事。

所以结论是,如果很多人不知道他们的钱在哪里(即需要很长时间来响应驱动程序提出的问题),异步控制器可以很好地帮助处理请求,从而加快处理过程。如果没有 aysnc 控制器,每个人都会等到前面的人被完全处理掉。但是不要忘记,在 MVC 中,您在单个总线上有很多总线驱动程序,因此异步不是自动选择。


非常好的比喻。谢谢你。
我喜欢这个描述。谢谢
很好的解释方式。谢谢,
您的回答结合 Darin 的回答总结了异步控制器背后的整个机制,它是什么,更重要的是它不是什么!
这是一个很好的类比,但我唯一的问题是:在你的类比中摸索口袋的家伙将是我们应用程序中的某种工作/处理......那么一旦我们释放线程,哪些进程可以工作? Surly这是另一个线程?那么我们在这里有什么收获呢?
M
Mikael Eliasson

这里有两个概念在起作用。首先,我们可以让我们的代码并行运行以更快地执行,或者将代码安排在另一个线程上以避免让用户等待。你的例子

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}

属于第二类。用户将获得更快的响应,但服务器上的总工作量更高,因为它必须做同样的工作 + 处理线程。

另一个例子是:

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Make async web request to twitter with WebClient.DownloadString()
    });

    Task.Factory.StartNew(() => { 
        //Make async web request to facebook with WebClient.DownloadString()
    });


    //wait for both to be ready and merge the results

    return View();
}

因为请求是并行运行的,所以用户不必等待,只要它们是串行完成的。但是你应该意识到,我们在这里比串行运行消耗更多的资源,因为我们在许多线程上运行代码,同时我们也有线程等待。

这在客户端场景中非常好。将同步长时间运行的代码包装在一个新任务中(在另一个线程上运行)也很常见,也可以保持 ui 响应或并行化以使其更快。尽管如此,一个线程仍然在整个持续时间内使用。在高负载的服务器上,这可能会适得其反,因为您实际上使用了更多资源。这是人们警告过你的

MVC 中的异步控制器还有另一个目标。这里的重点是避免让线程无所事事(这会损害可伸缩性)。仅当您调用的 API 具有异步方法时才重要。像 WebClient.DowloadStringAsync()。

关键是你可以让你的线程被返回来处理新的请求,直到网络请求完成,它会调用你的回调,得到相同的或新的线程并完成请求。

我希望您了解异步和并行之间的区别。将并行代码视为您的线程所在的代码并等待结果。虽然异步代码是在代码完成时通知您并且您可以重新开始工作的代码,同时线程可以执行其他工作。


P
Paul Turner

如果操作异步运行,应用程序可以更好地扩展,但前提是有资源可用于为其他操作提供服务。

异步操作确保您永远不会因为现有操作正在进行而阻止操作。 ASP.NET 具有允许并行执行多个请求的异步模型。可以将请求排队并处理它们 FIFO,但是当您有数百个请求排队并且每个请求需要 100 毫秒来处理时,这将无法很好地扩展。

如果您有大量流量,最好不要异步执行查询,因为可能没有额外的资源来服务请求。如果没有备用资源,您的请求将被迫排队,花费指数级更长或彻底失败,在这种情况下,异步开销(互斥锁和上下文切换操作)不会给您任何帮助。

就 ASP.NET 而言,您别无选择 - 它使用异步模型,因为这对服务器-客户端模型有意义。如果您要在内部编写自己的代码并使用异步模式尝试更好地扩展,除非您尝试管理在所有请求之间共享的资源,否则您实际上不会看到任何改进,因为它们已经被包装在不阻塞其他任何东西的异步过程中。

最终,在您真正查看导致系统瓶颈的原因之前,这一切都是主观的。有时很明显,异步模式会在哪些方面有所帮助(通过防止排队的资源阻塞)。最终,只有测量和分析系统才能表明您可以在哪里获得效率。

编辑:

在您的示例中,Task.Factory.StartNew 调用将使 .NET 线程池上的操作排队。线程池线程的本质是可以重复使用(以避免创建/销毁大量线程的成本)。操作完成后,线程将被释放回池以供另一个请求重新使用(垃圾收集器实际上不会参与,除非您在操作中创建了一些对象,在这种情况下,它们会按正常方式收集范围界定)。

就 ASP.NET 而言,这里没有什么特别的操作。 ASP.NET 请求在不考虑异步任务的情况下完成。唯一的问题可能是您的线程池是否已饱和(即,现在没有线程可用于为请求提供服务,并且池的设置不允许创建更多线程),在这种情况下,请求被阻止等待启动任务,直到池线程变得可用。


谢谢!阅读您的答案后,我使用代码示例编辑了问题。你能看看吗?
你有一个对我来说很神奇的句子:Task.Factory.StartNew 调用将使 .NET 线程池上的操作排队。。在这种情况下,这里哪个是正确的:1-) 它创建一个新线程,完成后,该线程返回线程池并在那里等待再次重用。 2-) 它从线程池中获取一个线程,然后该线程返回线程池并在那里等待再次被重用。 3-) 它采用最有效的方法,可以做到其中任何一种。
线程池根据需要创建线程,并在线程不使用时回收它们。它的确切行为因 CLR 版本而异。您可以在此处找到有关它的具体信息msdn.microsoft.com/en-us/library/0ka9477y.aspx
它现在开始在我的脑海中形成。所以,CLR 拥有线程池,对吧?例如,WPF 应用程序也有线程池的概念,它也处理线程池。
线程池是 CLR 中它自己的东西。其他“知道”池的组件表明它们使用线程池线程(在适当的情况下),而不是创建和销毁自己的线程。创建或销毁线程是一项相对昂贵的操作,因此使用池对于短期运行的操作来说是一个很大的效率提升。
A
A.R.

是的,他们使用线程池中的线程。实际上,MSDN 提供了一个非常出色的指南,可以解决您的所有问题等等。我发现它在过去非常有用。一探究竟!

http://msdn.microsoft.com/en-us/library/ee728598.aspx

同时,您听到的关于异步代码的评论和建议应该持保留态度。对于初学者来说,仅仅做一些异步的东西并不一定会让它扩展得更好,而且在某些情况下会使你的应用程序扩展得更糟。您发布的关于“大量流量......”的其他评论也仅在某些情况下是正确的。这实际上取决于您的操作在做什么,以及它们如何与系统的其他部分交互。

简而言之,很多人对异步有很多看法,但断章取义可能并不正确。我会说专注于您的确切问题,并进行基本的性能测试以查看异步控制器等实际处理您的应用程序。


我可能已经阅读了该文件数百次,但我仍然有很多困惑(也许问题出在我身上,谁知道呢)。当您环顾四周时,您会看到很多关于 ASP.NET MVC 上的异步的不一致评论,正如您在我的问题中所看到的那样。
对于最后一句话:在控制器操作中,我分别查询数据库 5 次(我必须这样做),这一切都花费了大约 400 毫秒。然后,我实现了 AsyncController 并并行运行它们。响应时间大大减少到大约。 200 毫秒。但我不知道它创建了多少线程,这些线程在我完成后会发生什么,GC 是否会在我完成后立即清理它们,以便我的应用程序不会发生内存泄漏,等等等等。关于那部分的任何想法。
附上一个调试器并找出答案。
S
Shivprasad Koirala

首先它不是 MVC,而是维护线程池的 IIS。因此,来自 MVC 或 ASP.NET 应用程序的任何请求都由线程池中维护的线程提供服务。只有使应用程序异步,他才会在不同的线程中调用此操作并立即释放线程,以便可以接受其他请求。

我已经用详细视频(http://www.youtube.com/watch?v=wvg13n5V0V0/“MVC 异步控制器和线程饥饿”)解释了相同的内容,它显示了 MVC 中的线程饥饿是如何发生的,以及如何通过使用 MVC 异步控制器将其最小化。我还使用 perfmon 测量了请求队列所以您可以看到 MVC 异步的请求队列如何减少,以及同步操作的最差情况如何。