ThreadPool 类提供了两个方法用来查询线程池的状态。第一个是我们可以从线程池获取当前可用的线程数量:
public static void GetAvailableThreads(
out int workerThreads,
out int completionPortThreads);
从方法中你可以看到两种不同的线程:
WorkerThreads
工作线程是标准系统池的一部分。它们是被.NET框架托管的标准线程,多数函数是在这里执行的。显式的用户请求(QueueUserWorkItem方法),基于异步对象的方法(RegisterWaitForSingleObject)和定时器(Timer类)
CompletionPortThreads
这种线程常常用来I/O操作,Windows NT, Windows 2000 和 Windows XP提供了一个步执行的对象,叫做IOCompletionPort。把API和异步对象关联起来,用少量的资源和有效的方法,我们就可以调用系统线程池的异步I/O操作。但是在Windows 95, Windows 98, 和 Windows Me有一些局限。比如: 在某些设备上,没有提供IOCompletionPorts 功能和一些异步操作,如磁盘和邮件槽。在这里你可以看到.NET框架的最大特色:一次编译,可以在多个系统下运行。根据不同的目标平台,.NET 框架会决定是否使用IOCompletionPorts API,用最少的资源达到最好的性能。
这节包含一个使用Socket 类的例子。在这个示例中,我们将异步建立一个连接到本地的Web服务器,然后发送一个Get请求。通过这个例子,我们可以很容易地鉴别这两种不同的线程。
using System;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ThreadPoolTest{
class MainApp
{
static void Main()
{
Socket s;
IPHostEntry hostEntry;
IPAddress ipAddress;
IPEndPoint ipEndPoint;
hostEntry = Dns.Resolve(Dns.GetHostName());
ipAddress = hostEntry.AddressList[0];
ipEndPoint = new IPEndPoint(ipAddress, 80);
s = new Socket(ipAddress.AddressFamily,SocketType.Stream, ProtocolType.Tcp);
s.BeginConnect(ipEndPoint, new AsyncCallback(ConnectCallback),s);
Console.ReadLine();
}
static void ConnectCallback(IAsyncResult ar)
{
byte[] data;
Socket s = (Socket)ar.AsyncState;
data = Encoding.ASCII.GetBytes("GET /\n");
Console.WriteLine("Connected to localhost:80");
ShowAvailableThreads();
s.BeginSend(data, 0,data.Length,SocketFlags.None,new AsyncCallback(SendCallback), null);
}
static void SendCallback(IAsyncResult ar)
{
Console.WriteLine("Request sent to localhost:80");
ShowAvailableThreads();
}
static void ShowAvailableThreads()
{
int workerThreads, completionPortThread;
ThreadPool.GetAvailableThreads(out workerThreads,out completionPortThreads);
Console.WriteLine("WorkerThreads: {0},"+" CompletionPortThreads: {1}",workerThreads, completionPortThreads);
}
}
}
如果你在Microsoft Windows NT, Windows 2000, or Windows XP 下运行这个程序,你将会看到如下结果:
Connected to localhost:80WorkerThreads: 24, CompletionPortThreads: 25Request sent to localhost:80WorkerThreads: 25, CompletionPortThreads: 24
如你所看到地那样,连接用了工作线程,而发送数据用了一个完成端口(CompletionPort),接着看下面的顺序:
1. 我们得到一个本地IP地址,然后异步连接到那里。
2. Socket在工作线程上执行异步连接操作,因为在Socket上,不能用Windows 的IOCompletionPorts来建立连接。
3. 一旦连接建立了,Socket类调用指明的函数ConnectCallback,这个回调函数显示了线程池中可用的线程数量。我们可以看到这些是在工作线程中执行的。
4. 在用ASCII码对Get请求进行编码后,我们用BeginSend方法从同样的函数ConnectCallback 中发送一个异步请求。
5. Socket上的发送和接收操作可以通过IOCompletionPort 来执行异步操作,所以当请求做完后,回调函数就会在一个CompletionPort类型的线程中执行。因为函数本身显示了可用的线程数量,所以我们可以通过这个来查看,对应的完成端口数已经减少了多少。
如果我们在Windows 95, Windows 98, 或者 Windows Me平台上运行相同的代码,会出现相同的连接结果,请求将被发送到工作线程,而非完成端口。你应该知道的很重要的一点就是,Socket类总是会利用最优的可用机制,所以你在开发应用时,可以不用考虑目标平台是什么。
你已经看到在上面的例子中每种类型的线程可用的最大数是25。我们可以用GetMaxThreads返回这个值:
public static void GetMaxThreads(
out int workerThreads,
out int completionPortThreads);
一旦到了最大的数量,就不会创建新线程,所有的请求都将被排队。假如你看过ThreadPool类的所有方法,你将发现没有一个允许我们更改最大数的方法。就像我们前面提到的那样,线程池是每个处理过程的唯一共享资源。这就是为什么不可能让应用程序域去更改这个配置的原因。想象一下出现这种情况的后果,如果有第三方组件把线程池中线程的最大数改为1,整个应用都会停止工作,甚至在进程中其它的应用程序域都将受到影响。同样的原因,公共语言运行时的宿主也有可能去更改这个配置。比如:ASP.NET允许系统管理员更改这个数字。
在你的应用程序使用线程池之前,还有一个东西你应该知道:死锁。在线程池中执行一个实现不好的异步对象可能导致你的整个应用系统中止运行。
设想你的代码中有个方法,它需要通过Socket连接到一个Web服务器上。一个可能的实现就是用Socket 类中的BeginConnect方法异步打开一个连接,然后用EndConnect方法等待连接的建立。代码如下:
class ConnectionSocket
{
public void Connect()
{
IPHostEntry ipHostEntry = Dns.Resolve(Dns.GetHostName());
IPEndPoint ipEndPoint = new IPEndPoint(ipHostEntry.AddressList[0],
80);
Socket s = new Socket(ipEndPoint.AddressFamily, SocketType.Stream,
ProtocolType.Tcp);
IAsyncResult ar = s.BeginConnect(ipEndPoint, null, null);
s.EndConnect(ar);
}
}
多快,多好。调用BeginConnect使异步操作在线程池中执行,而EndConnect一直阻塞到连接被建立。
如果线程池中的一个执行函数中用了这个类的方法,将会发生什么事情呢?设想线程池的大小只有两个线程,然后用我们的连接类创建了两个异步对象。当这两个函数同时在池中执行时,线程池已经没有用于其它请求的空间了,除非直到某个函数结束。问题是这些函数调用了我们类中的Connect方法,这个方法在线程池中又发起了一个异步操作。但线程池一直是满的,所以请求就一直等待任何空闲线程的出现。不幸的是,这将永远不会发生,因为使用线程池的函数正等待队列函数的结束。结论就是:我们的应用系统已经阻塞了。
我们以此推断25个线程的线程池的行为。假如25个函数都等待异步对象操作的结束。结果将是一样的,死锁一样会出现。
在下面的代码片断中,我们使用了这个类来说明问题:
class MainApp
{
static void Main()
{
for(int i=0;i<30;i++)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(PoolFunc));
}
Console.ReadLine();
}
static void PoolFunc(object state)
{
int workerThreads,completionPortThreads;
ThreadPool.GetAvailableThreads(out workerThreads,
out completionPortThreads);
Console.WriteLine("WorkerThreads: {0}, CompletionPortThreads: {1}",
workerThreads, completionPortThreads);
Thread.Sleep(15000);
ConnectionSocket connection = new ConnectionSocket();
connection.Connect();
}
}
如果你运行这个例子,你将看到池中的线程是如何把线程的可用数量减少到零的,接着应用中止,死锁出现了。
如果你想在你的应用中避免出现死锁,永远不要阻塞正在等待线程池中的其它函数的线程。这看起来很容易,但记住这个规则意味着有两条:
n 不要创建这样的类,它的同步方法在等待异步函数。因为这种类可能被线程池中的线程调用。
n 不要在任何异步函数中使用这样的类,如果它正等待着这个异步函数。
如果你想检测到应用中的死锁情况,那么就当你的系统挂起时,检查线程池中的线程可用数。线程的可用数量已经没有并且CPU的使用率为0 ,这是很明显的死锁症状。你应该检查你的代码,以确定哪个在线程中执行的函数正在等待异步操作,然后删除它。