下载本文的代码:NETMatters0412.exe (163KB)
问: 我和我的客户正在开发一个客户端应用程序,它需要向服务器应用程序发出 HttpWebRequest 以提交数据。我们正在尽力限制客户端发出的并发连接的数量,这样我们就可以限制服务器上的负载。最初我们尝试从 ThreadPool 线程发出这些请求,但是我们不断地接收到异常,这些异常指出“There were not enough free threads in the ThreadPool object to complete the operation。”因此,我有两个问题。第一,ThreadPool 为什么没有足够的线程?是否完全都是由于 ThreadPool 的原因,才会导致在有线程可用之前,已排队工作项被阻塞?第二,如果我们无法使用 ThreadPool 来完成这一任务,则我们如何限制所请求的并发连接数目?
答:这是一组很好的问题,快速地搜索一下 Web 即可看出,这个问题给许多开发人员造成了困扰。需要知道的第一件事情是,在 Microsoft.NET Framework 1.x 版中,HttpWebRequest 从来不会发出同步请求。我这样说是什么意思呢?让我们来看一看 Shared Source CLI (SSCLI) 中为 HttpWebRequest.GetResponse 编写的代码,此处显示的代码省略了查看以前是否检索了该响应的代码和计算超时的代码:
public override WebResponse GetResponse() { ••• IAsyncResult asyncResult = BeginGetResponse(null, null); ••• return EndGetResponse(asyncResult); }
您可以看出,HttpWebRequest.GetResponse 只是 BeginGetResponse 和 EndGetResponse 对周围的包装。它们是异步运行的,这意味着,BeginGetResponse 从中发出实际 HTTP 请求的线程不同于调用它的线程,而且在该请求完成之前,EndGetResponse 会阻塞。这样做的实际结果是,HttpWebRequest 将每个出站请求的 ThreadPool 排入工作项队列中。因此,我们知道 HttpWebRequest 使用来自 ThreadPool 的线程,但是为什么在从 ThreadPool 线程调用 GetResponse 时会产生问题呢?死锁。
正如您在问题所指出 的,ThreadPool 将对在有可用的线程之前一直等待的工作项进行排队。为了便于讨论,我们假定 ThreadPool 是仅有一个线程的池(默认情况下它当然要大得多)。您将一个调用 HttpWebRequest.GetResponse 的方法排入队列,该方法开始执行 ThreadPool 中的唯一线程。GetResponse 方法调用 BeginGetResponse,后者将一个工作项排入 ThreadPool 中,然后调用 EndGetResponse 来等待该工作项的完成。遗憾的是,这种情况永远都不会发生。在 ThreadPool 线程变得可用之前,排入队列的工作项 BeginGetResponse 不会执行,但是 ThreadPool 中的唯一线程当前正在执行对 EndGetResponse 的调用,从而等待 HTTP 请求的完成。死锁。“但是,”您说,“这是一个人为设想的例子,该池中仅有一个线程,如您所言,实际池中的线程数要多于这个数目。”确实如此,但让我们对 这个例子进行扩展。如果池中有两个线程,在这两个方法中的任意一个有机会排列其工作项之前,您将这两个调用 GetResponse 的方法快速排入队列中,那么会发生什么呢?还是相同的问题,死锁。如果池中有 25 个线程,并且您将这 25 个方法快速排入队列,那么会发生什么呢?又是死锁。明白这个问题了吗?
作为该问题的替代解决方案,Framework 小组实现了您正在研究的异常。在 BeginGetResponse 的最后(就在工作项被排入队列之前),使用 System.Net.Connection.IsThreadPoolLow 来查询池中有多少个可用工作线程。如果数量过少(少于 2 个),将会引发 InvalidOperationException。
好消息是,在 .NET Framework 2.0 中,用 HttpWebRequest.GetResponse 发出的同步请求是真正同步的,这样该问题就不复存在了,从而使得您可以将调用 GetResponse 的方法列入您的核心内容。不太好的消息是,您仍然会受到 1.x 版本中这一问题的困扰。一种解决方案是(正如您所指出的),显式限制已经排入队列的工作项的数量或在任何时间内在 ThreadPool 中执行的工作项的数量。要实现这一方法,需要有一种方法来跟踪当前有多少个未完成的工作项,并在达到预定界限之前,阻塞新的请求。
信 号是同步基元,它维持在零至某个最大数值之间的一个计数。提供了增加和减少该计数的操作,窍门在于,当该计数已经为零时,尝试降低该计数会导致调用线程被 阻塞,直到出现另一个线程和增加该计数为止。这使得信号可以很好地用于几种不同的目的。第一种情况(可能是大学操作系统课上讨论最多的情况之一)是生产者 /消费者模型,其中生产者线程生成供消费者线程在以后使用的产品。第二种情况(可能是第一种情况的一个超集)是控制对仅支持有限用户数量的共享资源的访 问。在这种情况中,信号用作一种保护,在当前访问某一资源的用户数达到所允许的最大值时,可以阻止用户对这一资源的访问尝试。对这种情况来说,听起来非常 完美,是吧?
事实上确实如此。遗憾的是,.NET Framework 1.x 版中不包含信号实现。但是,将用于 Win32® 信号对象的简单包装组合在一起只需要很少的代码,如图 1 所示(请注意,.NET Framework 版本 2.0 包括信号实现,而且包含一个远比此处所给出的类更完整的类)。DllImport 属性与 P/Invoke 一起使用,以包装 CreateSemaphore(从 kernel32.dll 中公开的 Win32 函数,用于实例化信号对象)和 ReleaseSemaphore(同样是从 kernel32.dll 公开的,用于增加信号的计数)。使用 CreateSemaphore 返回的句柄引用信号对象,从而为从 WaitHandle 中派生 Semaphore 类并使用其提供的功能(如 WaitOne)等待同步对象提供了最好的机会。使用基类的 WaitOne 会导致信号计数减少,如果该计数已经为零,则进行阻塞,直到该计数增加或者指定的超时已过为止。
通过此信号,现在可以非常容易地创建一个类来限制排入 ThreadPool 的项。这样的实现如图 2 所示。对此信号进行初始配置,使其拥有一个最大初始计数,该计数等于您希望同时允许执行的最大请求数。在调用 ThreadPoolThrottle.QueueUserWorkItem 时,调用该信号的 WaitOne 方法来降低计数,如果正在执行最大数目的请求,则进行阻止。正如 .NET 问题的 2004 年 10 月 部 分的 ThreadPoolWait 类一样,用户指定的 WaitCallback 及其相关状态打包在一个新的状态对象中,然后该对象作为私有方法 HandleWorkItem 的状态排入 ThreadPool 中。当被 ThreadPool 调用时,HandleWorkItem 会调用具有用户指定状态的用户指定委托。然后它增加该信号,以指示该工作单元已经执行完毕,从而允许被阻塞的线程醒来并继续其请求。假定没有其他任务同时 使用该池(从而减少可用线程数),可以使用一个像 ThreadPoolThrottle 这样的类来成功地限制已排队工作项的数量。
问:我正在编写一个使用反射的工具。对于一个类中的一个方法,我需要指出这个类方法实现哪个接口方法。如果它没有实现一个接口,我也需要知道这一结果。这可能吗?
答:首先,这个问题有点令人误解。您似乎在暗示一个类中的一个方法只能实现一个接口中的一个方法,但事实上这是不正确的。一个类方法可用于实现多个接口中的方法,甚至是同一接口的多个方法,当然,并不是所有语言都支持这一功能。以下面的 C# 示例为例:
interface SomeInterface1 { void Method1(); } interface SomeInterface2 { void Method1(); } class SomeClass : SomeInterface1, SomeInterface2 { public void Method1(){} }
这里,方法 SomeClass.Method1 实现两个接口方法 SomeInterface1.Method1 和 SomeInterface2.Method1,它们都使用隐式接口实现。C# 语言提供了两种可以将类方法映射到接口方法的方式。一种方式是通过显式接口成员实现,即通过用接口的名称和接口方法的名称命名类成员来进行指定,如下所 示:
class SomeClass : SomeInterface1 { void SomeInterface1.Method1() {} }
这使得该实现被公开,但只能通过此接口公开。第二种方式是隐式实现,即按照名称、类型和形参表将非静态公共方法与接口方法进行匹配来做 到这一点。同样,虽然在 C# 中有可能(如前所示)用一个类方法来实现两个不同接口的一个方法,但是在 C# 中不可能用一个类方法实现来自同一接口的两个不同的接口方法。这是因为在同一个接口中有两个名称、类型和形参均相同的方法是无效的,这种情况需要一个类方 法来映射到两个方法。Visual Basic .NET 通过强制要求开发人员显式声明类方法正在实现什么接口方法来解决这一问题。因而在 Visual Basic .NET 中,有可能用一个类方法来实现来自同一接口的两个不同的接口方法,如以下代码片段所示:
Interface SomeInterface Sub Method1() Sub Method2() End Interface Class SomeClass Implements SomeInterface Public Sub SomeMethod() Implements _ SomeInterface.Method1, SomeInterface.Method2 Console.WriteLine("SomeMethod called.") End Sub End Class
这里,SomeClass.SomeMethod 实际上实现了 SomeInterface.Method1 和 SomeInterface.Method2。
介绍完这些内容之后,让我们回过头讨论您的问题的核心。是的,使用反射来确定某个给定类方法实现什么接口方法是可能的,一种可能的解决方案如图 3 所示。
Type 类公开 GetInterfaceMap 方法,在传递一个表示接口的 Type 时,可以用该方法来返回一个映射,该映射表示如何将该接口映射到实现该接口的类的实际方法。GetInterfaceMap 返回的类型 InterfaceMap 公开两个重要字段:TargetMethods 和 InterfaceMethods。这些字段存储数组,前者是 MethodInfo 对象(表示实现接口方法的类型的方法)的一个数组,后者为映射到前一数组的接口方法。举例来说,TargetMethods[2] 的 MethodInfo 表示的方法实现存储在 InterfaceMethods[2] 中的 MethodInfo 表示的接口方法。
图 3 显示的 GetImplementedInterfaces 方法接受表示相关方法的 MethodInfo,它返回 Type 实例的一个数组,其中的每个实例都表示使用该方法实现的一个接口。首先,检索该方法的 ReflectedType,这对于检索该类型实现的所有接口以及该类型的方法的接口映射都是必要的。您可能会注意到,除 ReflectedType 之外,MemberInfo 类(MethodInfo 派生于该类)还公开了 DeclaringType 属性。我没有将 DeclaringType 用于此目的,这是很重要的。不同之处在于,DeclaringType 返回声明此成员的类型,而 ReflectedType 返回用于检索给定 MethodInfo 的类型。它们何时不同?为什么这种区别非常重要呢?对于由此方法的容器类实现的每个接口,我都需要依次通过接口映射中的每个目标方法,以查看指定的方法是 否与其中任何一个相匹配。如果匹配,则指定的方法就是在给定接口的接口映射中,这意味着此方法可以用于实现该接口。然而在某些情况下,您可以获得接口实现 方法的 MethodInfo,但是在其中,MethodInfo 与 TargetMethods 数组中的任何 MethodInfo 均不匹配。具体而言,当声明类型与反射类型不同时可能会发生这一情况。考虑以下示例:
interface SomeInterface { void Method1(); } class Base : SomeInterface { public void Method1() {} } class Derived : Base {}
Derived.Method1 实际上是在 Base 类中实现的。结果,使用以下代码检索的 MethodInfo
typeof(Derived).GetMethod("Method1")
是一个不同于使用以下代码检索的 MethodInfo 的对象
typeof(Base).GetMethod("Method1")
如 果我要使用 Base Type(在本例中是声明 Method1 的类型)的 GetInterfaceMap 来检索接口映射,但已经使用 typeof(Derived) 检索 MethodInfo,则 MethodInfo 将不会与 TargetMethods 数组中的任何一个对象相匹配。因此,重要的是调用用于检索指定 MethodInfo 的相同 Type,该 Type 是从 MethodInfo 的 ReflectedType 属性返回的。
问:我最近阅读了有关 COM 线程模型以及 .NET 如何适合图片的内容。我知道可以使用 STAThreadAttribute 和 MTAThreadAttribute 为应用程序的主线程初始设置线程模型,我也知道可以使用 Thread.ApartmentState 属性来配置一个线程,但这两种技术好像只能在 COM 对象被调用之前正常工作。在调用之后,它们似乎就不可变了。真是这样吗?如果是这样,那么在我需要使用多个 COM 对象而每个对象又使用不同的线程模型时,该怎么做?
图 3 答: 在第一次调用 COM 对象之前,可以任意频繁地更改 Thread.ApartmentState 属性。此时,当前线程的线程模型变得不可变,您不能再更改它。如果您要使用的 COM 对象需要的线程模型不同于当前线程的线程模型,则标准的解决方案是采用一个新的线程,恰当地设置其 ApartmentState,然后执行使用该新线程上的 COM 对象的代码。(有关 COM 线程模型中刷新器的信息,我推荐阅读 Larry Osterman 的网络日记帖子 What are these "Threading Models" and why do I care? 。)
为了使该过程简单一点,我已经实现了如图 4 所示的类。ApartmentStateSwitcher 仅公开一个名为 Execute 的静态方法。此方法获取要调用的委托、要传递给该委托的参数,以及执行该委托所需要的 ApartmentState。如果当前线程的 ApartmentState 与用户所请求的匹配,则该方法只需使用该委托的 DynamicInvoke 方法(该方法是为后期绑定方法调用提供的)调用该委托。然而,如果单元状态不同,则它将调用具有正确设置的 ApartmentState 的新线程上的委托。为此,它创建一个小对象,该对象可以用于在当前线程和新线程之间来回传递状态。它将该委托以及用于调用该委托的参数存储在此对象中。
然 后用适当的 ApartmentState 创建一个新线程,并启动该线程,之后立即将其与当前线程联接,并在新线程完成执行之前阻塞当前线程。该新线程的主方法是 Run,它是执行带有所提供参数的委托的类的一个私有成员。如果在这一过程引发任何异常,则来自委托调用的任何返回值都将存储在状态类中(在 .NET Framework 的 1.x 版本中,辅助线程上引发的异常仅仅由运行时解决,而在 2.0 版中,辅助线程异常将导致 AppDomain 关闭;在这两种版本中,异常都不会传回主线程,因此我必须手动完成这一操作)。回到 Execute 方法,当辅助线程完成时,Thread.Join 调用完成,而且系统会检查状态类以查看异常和返回值。如果存在异常,它将重新引发。如果没有异常,则返回来自该委托的返回值。
该类的用法非常简单,您只需为您需要执行的任何代码创建一个委托(在 C# 2.0 中,使用匿名委托会使此过程简单得多),并将该委托传递给 ApartmentStateSwitcher.Execute 即可,如下所示:
[MTAThread] static void Main(string[] args) { PrintCurrentState(); ApartmentStateSwitcher.Execute( new ThreadStart(PrintCurrentState), null, ApartmentState.STA); PrintCurrentState(); } static void PrintCurrentState() { Console.WriteLine("Thread apartment state: " + Thread.CurrentThread.ApartmentState); }
此代码将以下内容打印到控制台:
Thread apartment state: MTA Thread apartment state: STA Thread apartment state: MTA
请将您的问题和意见发送到 [email protected] 。
Stephen Toub 是 MSDN 的技术编辑。