我能否在IIS上使用线程执行长时间运行的作业?

89
在ASP.Net应用程序中,用户在网页上点击按钮,这会通过事件处理程序实例化服务器上的对象并在该对象上调用方法。 该方法转到外部系统执行操作,这可能需要一段时间。因此,我希望在另一个线程中运行该方法调用,以便可以返回“已提交您的请求”消息并将控制权返回给用户。 尽管我很满意这样做是“fire-and-forget”,但如果用户可以继续轮询对象状态,那就更好了。
我不知道IIS是否允许我的线程继续运行,即使用户会话已过期。 想象一下,用户触发事件,我们在服务器上实例化对象并在新线程中触发方法。用户收到“已提交您的请求”消息,并关闭浏览器。最终,该用户的会话将在IIS上超时,但线程可能仍在运行工作。IIS会允许线程继续运行还是会在用户会话过期后终止并释放该对象?

1
你有关于需要处理的内容和托管环境的更多细节吗?例如,您能否安装服务? - senfo
你可以始终使用Async和Await编程方法(Visual Studio 2012+)。比自己管理线程更加简洁。 - SausageFingers
10个回答

67
您可以实现您想要的,但通常这是一个不好的主意。几个ASP.NET博客和CMS引擎采用了这种方法,因为它们希望能够安装在共享托管系统上,并且不依赖于需要安装的Windows服务。通常情况下,当应用程序启动时,它们会在Global.asax中启动一个长时间运行的线程,并由该线程处理排队的任务。
除了减少IIS/ASP.NET可用于处理请求的资源之外,您还将面临线程在AppDomain重新启动时被终止的问题,然后必须处理任务在进行时的持久性,以及在AppDomain重新启动时重新启动工作。
请记住,在许多情况下,默认间隔自动重新启动AppDomain,以及如果更新web.config等。
如果您可以处理线程随时被杀死的持久性和事务方面,那么您可以通过具有某些外部进程的持续请求来避免AppDomain重复使用-因此,如果站点重复使用,则可以保证在X分钟内自动重新启动。
再次强调,这通常是一个不好的主意。
编辑:以下是此技术的一些示例: Community Server:使用Windows服务与后台线程定期运行代码 在网站首次启动时创建后台线程

编辑(来自遥远的未来)- 如今我会使用Hangfire


1
谢谢,这很有见地。回收利用绝对是个问题,我从你的话中理解到我可以将其作为一项固定的事情来做,但是这是很危险的。 - flytzen
在我的情况下,我能够告诉IIS永远不要回收并且永远不要使站点空闲。这使得我能够在这种假设下工作,并且该站点只会在部署和计划维护期间被回收。由于我只是在做一个管理类型的站点,所以对我来说非常完美。 - Brett
1
很奇怪这个帖子有这么多赞。ASP.NET的真正厉害之处在于这个东西是可行的:所有请求都在同一个AppDomain中服务,一起处理你创建的任何后台线程。给出的这些所谓“不好的想法”的原因都没有说服我。AppDomains可能会崩溃,是的——但这并不仅限于ASP.NET环境。你需要和你的托管环境相处融洽(搜索HostingEnvironment.RegisterObject)。 - John
3
.NET 4.5.2新增了一个新功能QueueBackgroundWorkItem,可以轻松注册后台任务,你可能需要更新你的答案。 - Scott Chamberlain
显示剩余4条评论

42

我不同意被接受的答案。

在ASP.NET中,使用后台线程(或使用Task.Factory.StartNew启动的任务)是可以的。与所有托管环境一样,您可能需要理解并配合管理关闭的设施。

在ASP.NET中,您可以使用HostingEnvironment.RegisterObject方法注册需要在关闭时优雅地停止的工作。有关讨论,请参见此文章和评论。

(正如杰拉德在他的评论中指出的那样,现在也有HostingEnvironment.QueueBackgroundWorkItem,它调用RegisterObject来注册后台项目的调度器。总体而言新方法更好,因为基于任务)。

至于您经常听到的不好的想法的一般主题,请考虑部署Windows服务(或其他类型的额外进程应用程序):

  • 不再使用Web Deploy进行平凡的部署
  • 不能纯粹在Azure网站上部署
  • 根据后台任务的性质,进程很可能需要通信。这意味着要么某种形式的IPC,要么服务将不得不访问常用数据库。

还要注意,一些高级场景甚至需要在同一地址空间中运行后台线程。我认为ASP.NET能够做到这一点是通过.NET变得可能的一个巨大优势。


这太棒了。我有一个任务需要大约30秒才能完成。如果您只需要延迟应用程序池足够长的时间来完成工作,那就非常完美。 - Scuba Steve
1
从 .Net Framework 4.5.2 开始,您还可以使用 HostingEnvironment.QueueBackgroundWorkItem(Action<CancellationToken>)。 - Gerard Carbó
根据我的观察,似乎IIS最终会关闭我正在运行的ASP.NET Web API,并且在新请求到来之前不会重新启动它。因此,在我的线程应该启动应用程序的指定时间,可能甚至没有运行。我对此有误吗? - David Sopko

7
我建议使用HangFire来满足这样的需求。它是一个不错的后台运行引擎,支持不同的架构,并且由于有持久性存储的支持,所以非常可靠。

7

您不希望使用来自IIS线程池的线程来执行此任务,因为这会导致该线程无法处理未来的请求。您可以查看 ASP.NET 2.0中的异步页面,但那真的也不是正确的答案。相反,听起来您会受益于研究 Microsoft Message Queuing。基本上,您将任务详细信息添加到队列中,另一个后台进程(可能是Windows服务)将负责执行该任务。但最重要的是,后台进程与IIS完全隔离。


啊,MSMQ - 我长期以来最喜欢的技术之一。感谢您的见解和链接。 - flytzen

5

这里有一个很好的帖子和示例代码:http://forums.asp.net/t/1534903.aspx?PageIndex=2

我甚至考虑过从线程中调用我的网站上的保持活动页面,以帮助保持应用程序池的活跃。请记住,如果您使用此方法,则需要非常好的恢复处理,因为应用程序可能随时重新启动。正如许多人所提到的,如果您可以访问其他服务选项,则这不是正确的方法,但对于共享托管来说,这可能是您唯一的选择之一。

为了帮助保持应用程序池的活跃,您可以在线程正在处理时向自己的网站发出请求。如果您的进程运行时间很长,这可能有助于保持应用程序池的活跃。

string tempStr = GetUrlPageSource("http://www.mysite.com/keepalive.aspx");


    public static string GetUrlPageSource(string url)
    {
        string returnString = "";

        try
        {
            Uri uri = new Uri(url);
            if (uri.Scheme == Uri.UriSchemeHttp)
            {
                HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri);
                CookieContainer cookieJar = new CookieContainer();

                req.CookieContainer = cookieJar;

                //set the request timeout to 60 seconds
                req.Timeout = 60000;
                req.UserAgent = "MyAgent";

                //we do not want to request a persistent connection
                req.KeepAlive = false;

                HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
                Stream stream = resp.GetResponseStream();
                StreamReader sr = new StreamReader(stream);

                returnString = sr.ReadToEnd();

                sr.Close();
                stream.Close();
                resp.Close();
            }
        }
        catch
        {
            returnString = "";
        }

        return returnString;
    }

5
我们开始尝试这条路线,当我们的应用程序在一个服务器上时,它实际上运行得很好。当我们想要扩展到多台机器(或在Web花园中使用多个w3wp)时,我们不得不重新评估并查看如何管理工作队列、错误处理、重试以及正确锁定以确保只有一个服务器接下来处理下一个项目的棘手问题。
... 我们意识到我们不是编写后台处理引擎的业务,因此我们寻找现有的解决方案,并最终使用了令人惊叹的OSS项目hangfire
Sergey Odinokov创建了一个真正的宝石,非常容易入门,并允许您交换工作持久性和排队的后端。Hangfire使用后台线程,但持久化作业,处理重试并使您能够查看工作队列。因此,Hangfire作业是强大且可以经受住所有应用程序域被回收等各种变数的考验。
其基本设置使用SQL Server作为存储,但在扩展时可以切换为Redis或MSMQ。它还具有出色的UI,可可视化显示所有作业及其状态,同时允许您重新排队作业。
我的观点是,虽然在后台线程中完成你想要的工作完全是可能的,但要使其可扩展和健壮需要大量工作。对于简单的工作负载来说没问题,但当事情变得更加复杂时,我更喜欢使用专门构建的库而不是经历这种努力。
为了更好地了解可用选项,可以查看Scott Hanselman的博客,其中涵盖了处理asp.net后台作业的几个选项。(他给了Hangfire一个极高的评价)
此外,正如John所引用的,值得阅读Phil Haack的博客,了解为什么这种方法存在问题以及如何在应用程序域卸载时优雅地停止线程上的工作。

2
您可以在后台运行任务,即使请求结束,它们也会完成。请注意不要抛出未捕获的异常。通常情况下,您希望始终抛出异常。如果在新线程中抛出异常,则会导致IIS工作进程 - w3wp.exe崩溃,因为您不再处于请求的上下文中。这也将杀死您正在运行的任何其他后台任务,以及如果您正在使用它们,则会杀死任何进程内存支持的会话。这将很难诊断,这就是为什么不鼓励这种做法的原因。

2

您可以创建一个Windows服务来完成这项任务吗?然后使用.NET远程调用从Web服务器调用Windows服务执行操作?如果是这种情况,那就是我会做的。

这将消除对IIS的依赖,并限制其处理能力。

如果不是这样,那么我会强制用户在处理过程中坐在那里。这样可以确保它被完成并且不被IIS杀死。


2

在IIS中,似乎有一种支持长时间运行工作的方式。 工作流服务似乎是为此而设计的,特别是与Windows Server AppFabric结合使用。该设计支持自动持久化和恢复长时间运行的工作,从而允许应用程序池进行回收。


1
只需创建一个替代进程来运行异步任务; 它不一定是Windows服务(尽管在大多数情况下这是更优化的方法)。 MSMQ过于复杂。

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