一、关于CLR线程池
使用ThreadStart与ParameterizedThreadStart建立新线程非常简单,但通过此方法建立的线程难于管理,若建立过多的线程反而会影响系统的性能。
有见及此,.NET引入CLR线程池这个概念。CLR线程池并不会在CLR初始化的时候立刻建立线程,而是在应用程序要创建线程来执行任务时,线程池才初始化一个线程。线程的初始化与其他的线程一样。在完成任务以后,该线程不会自行销毁,而是以挂起的状态返回到线程池。直到应用程序再次向线程池发出请求时,线程池里挂起的线程就会再度激活执行任务。这样既节省了建立线程所造成的性能损耗,也可以让多个任务反复重用同一线程,从而在应用程序生存期内节约大量开销。
注意:通过CLR线程池所建立的线程总是默认为后台线程,优先级数为ThreadPriority.Normal。
CLR线程池分为工作者线程(workerThreads)与I/O线程 (completionPortThreads) 两种,工作者线程是主要用作管理CLR内部对象的运作,I/O(Input/Output) 线程顾名思义是用于与外部系统交换信息。
通过ThreadPool.GetMax(out int workerThreads,out int completionPortThreads )和 ThreadPool.SetMax( int workerThreads, int completionPortThreads)两个方法可以分别读取和设置CLR线程池中工作者线程与I/O线程的最大线程数。在Framework2.0中最大线程默认为25*CPU数,在Framewok3.0、4.0中最大线程数默认为250*CPU数,在近年 I3,I5,I7 CPU出现后,线程池的最大值一般默认为1000、2000。
若想测试线程池中有多少的线程正在投入使用,可以通过ThreadPool.GetAvailableThreads( out int workerThreads,out int completionPortThreads ) 方法。
二、 CLR线程池的工作者线程
工作者线程是主要用作管理CLR内部对象的运作。
使用CLR线程池的工作者线程一般有两种方式:
1)直接通过 ThreadPool.QueueUserWorkItem() 方法
2)二是通过委托,下面将逐一细说。
2.1 通过QueueUserWorkItem启动工作者线程
ThreadPool线程池中包含有两个静态方法可以直接启动工作者线程:
1)为 ThreadPool.QueueUserWorkItem(WaitCallback)
2)为 ThreadPool.QueueUserWorkItem(WaitCallback,Object)
先把WaitCallback委托指向一个带有Object参数的无返回值方法,
再使用 ThreadPool.QueueUserWorkItem(WaitCallback) 就可以异步启动此方法,此时异步方法的参数被视为null 。
class Program
{
static void Main(string[] args)
{
//把CLR线程池的最大值设置为1000
ThreadPool.SetMaxThreads(1000, 1000);
//显示主线程启动时线程池信息
ThreadMessage("Start");
//启动工作者线程
ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncCallback));
Console.ReadKey();
}
static void AsyncCallback(object state)
{
Thread.Sleep(200);
ThreadMessage("AsyncCallback");
Console.WriteLine("Async thread do work!");
}
//显示线程现状
static void ThreadMessage(string data)
{
string message = string.Format("{0}\n CurrentThreadId is {1}",
data, Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
}
}
使用 ThreadPool.QueueUserWorkItem(WaitCallback,Object) 方法可以把object对象作为参数传送到回调函数中。
下面例子中就是把一个string对象作为参数发送到回调函数当中。
class Program
{
static void Main(string[] args)
{
//把线程池的最大值设置为1000
ThreadPool.SetMaxThreads(1000, 1000);
ThreadMessage("Start");
ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncCallback),"Hello Elva");
Console.ReadKey();
}
static void AsyncCallback(object state)
{
Thread.Sleep(200);
ThreadMessage("AsyncCallback");
string data = (string)state;
Console.WriteLine("Async thread do work!\n"+data);
}
//显示线程现状
static void ThreadMessage(string data)
{
string message = string.Format("{0}\n CurrentThreadId is {1}",
data, Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
}
}
通过ThreadPool.QueueUserWorkItem启动工作者线程虽然是方便,但WaitCallback委托指向的必须是一个带有Object参数的无返回值方法,这无疑是一种限制。若方法需要有返回值,或者带有多个参数,这将多费周折。有见及此,.NET提供了另一种方式去建立工作者线程,那就是委托。
2.2 利用委托类BeginInvoke与EndInvoke完成异步委托方法
使用CLR线程池中的工作者线程,最灵活最常用的方式就是使用委托的异步方法。
利用BeginInvoke与EndInvoke完成异步委托方法
首先建立一个委托对象,通过IAsyncResult BeginInvoke(string name,AsyncCallback callback,object state) 异步调用委托方法,BeginInvoke 方法除最后的两个参数外,其它参数都是与方法参数相对应的。通过 BeginInvoke 方法将返回一个实现了 System.IAsyncResult 接口的对象,之后就可以利用EndInvoke(IAsyncResult ) 方法就可以结束异步操作,获取委托的运行结果。
class Program
{
delegate string MyDelegate(string name);
static void Main(string[] args)
{
ThreadMessage("Main Thread");
//建立委托
MyDelegate myDelegate = new MyDelegate(Hello);
//异步调用委托,获取计算结果
IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null);
//完成主线程其他工作
.............
//等待异步方法完成,调用EndInvoke(IAsyncResult)获取运行结果
string data=myDelegate.EndInvoke(result);
Console.WriteLine(data);
Console.ReadKey();
}
static string Hello(string name)
{
ThreadMessage("Async Thread");
Thread.Sleep(2000); //虚拟异步工作
return "Hello " + name;
}
//显示当前线程
static void ThreadMessage(string data)
{
string message = string.Format("{0}\n ThreadId is:{1}",
data,Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
}
}
2.3 善用IAsyncResult
在以上例子中可以看见,如果在使用myDelegate.BeginInvoke后立即调用myDelegate.EndInvoke,那在异步线程未完成工作以前主线程将处于阻塞状态,等到异步线程结束获取计算结果后,主线程才能继续工作,这明显无法展示出多线程的优势。此时可以好好利用IAsyncResult 提高主线程的工作性能,IAsyncResult有以下成员:
public interface IAsyncResult
{
object AsyncState {get;} //获取用户定义的对象,它限定或包含关于异步操作的信息。
WailHandle AsyncWaitHandle {get;} //获取用于等待异步操作完成的 WaitHandle。
bool CompletedSynchronously {get;} //获取异步操作是否同步完成的指示。
bool IsCompleted {get;} //获取异步操作是否已完成的指示。
}
通过轮询方式,使用IsCompleted属性判断异步操作是否完成,这样在异步操作未完成前就可以让主线程执行另外的工作。
class Program
{
delegate string MyDelegate(string name);
static void Main(string[] args)
{
ThreadMessage("Main Thread");
//建立委托
MyDelegate myDelegate = new MyDelegate(Hello);
//异步调用委托,获取计算结果
IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null);
//在异步线程未完成前执行其他工作
while (!result.IsCompleted)
{
Thread.Sleep(200); //虚拟操作
Console.WriteLine("Main thead do work!");
}
string data=myDelegate.EndInvoke(result);
Console.WriteLine(data);
Console.ReadKey();
}
static string Hello(string name)
{
ThreadMessage("Async Thread");
Thread.Sleep(2000);
return "Hello " + name;
}
static void ThreadMessage(string data)
{
string message = string.Format("{0}\n ThreadId is:{1}",
data,Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
}
}
除此以外,也可以使用WailHandle完成同样的工作,WaitHandle里面包含有一个方法WaitOne(int timeout),它可以判断委托是否完成工作,在工作未完成前主线程可以继续其他工作。运行下面代码可得到与使用 IAsyncResult.IsCompleted 同样的结果,而且更简单方便 。
class Program
{
delegate string MyDelegate(string name);
static void Main(string[] args)
{
ThreadMessage("Main Thread");
//建立委托
MyDelegate myDelegate = new MyDelegate(Hello);
//异步调用委托,获取计算结果
IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null);
while (!result.AsyncWaitHandle.WaitOne(200))
{
Console.WriteLine("Main thead do work!");
}
string data=myDelegate.EndInvoke(result);
Console.WriteLine(data);
Console.ReadKey();
}
static string Hello(string name)
{
ThreadMessage("Async Thread");
Thread.Sleep(2000);
return "Hello " + name;
}
static void ThreadMessage(string data)
{
string message = string.Format("{0}\n ThreadId is:{1}",
data,Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
}
}
当要监视多个运行对象的时候,使用IAsyncResult.WaitHandle.WaitOne可就派不上用场了。
幸好.NET为WaitHandle准备了另外两个静态方法:WaitAny(waitHandle[], int)与WaitAll (waitHandle[] , int)。
其中WaitAll在等待所有waitHandle完成后再返回一个bool值。
而WaitAny是等待其中一个waitHandle完成后就返回一个int,这个int是代表已完成waitHandle在waitHandle[]中的数组索引。
下面就是使用WaitAll的例子,运行结果与使用 IAsyncResult.IsCompleted 相同。
class Program
{
delegate string MyDelegate(string name);
static void Main(string[] args)
{
ThreadMessage("Main Thread");
//建立委托
MyDelegate myDelegate = new MyDelegate(Hello);
//异步调用委托,获取计算结果
IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null);
//此处可加入多个检测对象
WaitHandle[] waitHandleList = new WaitHandle[] { result.AsyncWaitHandle,........ };
while (!WaitHandle.WaitAll(waitHandleList,200))
{
Console.WriteLine("Main thead do work!");
}
string data=myDelegate.EndInvoke(result);
Console.WriteLine(data);
Console.ReadKey();
}
static string Hello(string name)
{
ThreadMessage("Async Thread");
Thread.Sleep(2000);
return "Hello " + name;
}
static void ThreadMessage(string data)
{
string message = string.Format("{0}\n ThreadId is:{1}",
data,Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
}
}
2.4 回调函数
使用轮询方式来检测异步方法的状态非常麻烦,而且效率不高,有见及此,.NET为 IAsyncResult BeginInvoke(AsyncCallback , object)准备了一个回调函数。使用 AsyncCallback 就可以绑定一个方法作为回调函数,回调函数必须是带参数 IAsyncResult 且无返回值的方法: void AsycnCallbackMethod(IAsyncResult result) 。在BeginInvoke方法完成后,系统就会调用AsyncCallback所绑定的回调函数,最后回调函数中调用 XXX EndInvoke(IAsyncResult result) 就可以结束异步方法,它的返回值类型与委托的返回值一致。
class Program
{
delegate string MyDelegate(string name);
static void Main(string[] args)
{
ThreadMessage("Main Thread");
//建立委托
MyDelegate myDelegate = new MyDelegate(Hello);
//异步调用委托,获取计算结果
myDelegate.BeginInvoke("Leslie", new AsyncCallback(Completed), null);
//在启动异步线程后,主线程可以继续工作而不需要等待
for (int n = 0; n < 6; n++)
Console.WriteLine(" Main thread do work!");
Console.WriteLine("");
Console.ReadKey();
}
static string Hello(string name)
{
ThreadMessage("Async Thread");
Thread.Sleep(2000); \\模拟异步操作
return "\nHello " + name;
}
static void Completed(IAsyncResult result)
{
ThreadMessage("Async Completed");
//获取委托对象,调用EndInvoke方法获取运行结果
AsyncResult _result = (AsyncResult)result;
MyDelegate myDelegate = (MyDelegate)_result.AsyncDelegate;
string data = myDelegate.EndInvoke(_result);
Console.WriteLine(data);
}
static void ThreadMessage(string data)
{
string message = string.Format("{0}\n ThreadId is:{1}",
data, Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
}
}
可以看到,主线在调用BeginInvoke方法可以继续执行其他命令,而无需再等待了,这无疑比使用轮询方式判断异步方法是否完成更有优势。
在异步方法执行完成后将会调用AsyncCallback所绑定的回调函数,注意一点,回调函数依然是在异步线程中执行,这样就不会影响主线程的运行,这也使用回调函数最值得青昧的地方。
在回调函数中有一个既定的参数IAsyncResult,把IAsyncResult强制转换为AsyncResult后,就可以通过 AsyncResult.AsyncDelegate 获取原委托,再使用EndInvoke方法获取计算结果。
如果想为回调函数传送一些外部信息,就可以利用BeginInvoke(AsyncCallback,object)的最后一个参数object,它允许外部向回调函数输入任何类型的参数。只需要在回调函数中利用 AsyncResult.AsyncState 就可以获取object对象。
class Program
{
public class Person
{
public string Name;
public int Age;
}
delegate string MyDelegate(string name);
static void Main(string[] args)
{
ThreadMessage("Main Thread");
//建立委托
MyDelegate myDelegate = new MyDelegate(Hello);
//建立Person对象
Person person = new Person();
person.Name = "Elva";
person.Age = 27;
//异步调用委托,输入参数对象person, 获取计算结果
myDelegate.BeginInvoke("Leslie", new AsyncCallback(Completed), person);
//在启动异步线程后,主线程可以继续工作而不需要等待
for (int n = 0; n < 6; n++)
Console.WriteLine(" Main thread do work!");
Console.WriteLine("");
Console.ReadKey();
}
static string Hello(string name)
{
ThreadMessage("Async Thread");
Thread.Sleep(2000);
return "\nHello " + name;
}
static void Completed(IAsyncResult result)
{
ThreadMessage("Async Completed");
//获取委托对象,调用EndInvoke方法获取运行结果
AsyncResult _result = (AsyncResult)result;
MyDelegate myDelegate = (MyDelegate)_result.AsyncDelegate;
string data = myDelegate.EndInvoke(_result);
//获取Person对象
Person person = (Person)result.AsyncState;
string message = person.Name + "'s age is " + person.Age.ToString();
Console.WriteLine(data+"\n"+message);
}
static void ThreadMessage(string data)
{
string message = string.Format("{0}\n ThreadId is:{1}",
data, Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
}
}
三、CLR线程池的I/O线程
I/O 线程是.NET专为访问外部资源所设置的一种线程,因为访问外部资源常常要受到外界因素的影响,为了防止让主线程受影响而长期处于阻塞状态,.NET为多个I/O操作都建立起了异步方法,例如:FileStream、TCP/IP、WebRequest、WebService等等,而且每个异步方法的使用方式都非常类似,都是以BeginXXX为开始,以EndXXX结束,下面为大家一一解说。
3.1 异步读写 FileStream
需要在 FileStream 异步调用 I/O线程,必须使用以下构造函数建立 FileStream 对象,并把useAsync设置为 true。
FileStream stream = new FileStream ( string path, FileMode mode, FileAccess access, FileShare share, int bufferSize,bool useAsync ) ;
其中 path 是文件的相对路径或绝对路径; mode 确定如何打开或创建文件; access 确定访问文件的方式; share 确定文件如何进程共享; bufferSize 是代表缓冲区大小,一般默认最小值为8,在启动异步读取或写入时,文件大小一般大于缓冲大小; userAsync代表是否启动异步I/O线程。
注意:当使用 BeginRead 和 BeginWrite 方法在执行大量读或写时效果更好,但对于少量的读/写,这些方法速度可能比同步读取还要慢,因为进行线程间的切换需要大量时间。
3.1.1 异步写入
FileStream中包含BeginWrite、EndWrite 方法可以启动I/O线程进行异步写入。
public override IAsyncResult BeginWrite ( byte[] array, int offset, int numBytes, AsyncCallback userCallback, Object stateObject )
public override void EndWrite (IAsyncResult asyncResult )
BeginWrite 返回值为IAsyncResult, 使用方式与委托的BeginInvoke方法相似,最好就是使用回调函数,避免线程阻塞。在最后两个参数中,参数AsyncCallback用于绑定回调函数; 参数Object用于传递外部数据。要注意一点:AsyncCallback所绑定的回调函数必须是带单个 IAsyncResult 参数的无返回值方法。
在例子中,把FileStream作为外部数据传递到回调函数当中,然后在回调函数中利用IAsyncResult.AsyncState获取FileStream对象,最后通过FileStream.EndWrite(IAsyncResult)结束写入。
class Program
{
static void Main(string[] args)
{
//把线程池的最大值设置为1000
ThreadPool.SetMaxThreads(1000, 1000);
ThreadPoolMessage("Start");
//新立文件File.sour
FileStream stream = new FileStream("File.sour", FileMode.OpenOrCreate,
FileAccess.ReadWrite,FileShare.ReadWrite,1024,true);
byte[] bytes = new byte[16384];
string message = "An operating-system ThreadId has no fixed relationship........";
bytes = Encoding.Unicode.GetBytes(message);
//启动异步写入
stream.BeginWrite(bytes, 0, (int)bytes.Length,new AsyncCallback(Callback),stream);
stream.Flush();
Console.ReadKey();
}
static void Callback(IAsyncResult result)
{
//显示线程池现状
Thread.Sleep(200);
ThreadPoolMessage("AsyncCallback");
//结束异步写入
FileStream stream = (FileStream)result.AsyncState;
stream.EndWrite(result);
stream.Close();
}
//显示线程池现状
static void ThreadPoolMessage(string data)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("{0}\n CurrentThreadId is {1}\n "+
"WorkerThreads is:{2} CompletionPortThreads is :{3}",
data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
3.1.2 异步读取
FileStream 中包含 BeginRead 与 EndRead 可以异步调用I/O线程进行读取。
public override IAsyncResult BeginRead ( byte[] array,int offset,int numBytes, AsyncCallback userCallback,Object stateObject)
public override int EndRead(IAsyncResult asyncResult)
其使用方式与BeginWrite和EndWrite相似,AsyncCallback用于绑定回调函数; Object用于传递外部数据。在回调函数只需要使用IAsyncResut.AsyncState就可获取外部数据。EndWrite 方法会返回从流读取到的字节数量。
首先定义 FileData 类,里面包含FileStream对象,byte[] 数组和长度。然后把FileData对象作为外部数据传到回调函数,在回调函数中,把IAsyncResult.AsyncState强制转换为FileData,然后通过FileStream.EndRead(IAsyncResult)结束读取。最后比较一下长度,若读取到的长度与输入的数据长度不一至,则抛出异常。
class Program
{
public class FileData
{
public FileStream Stream;
public int Length;
public byte[] ByteData;
}
static void Main(string[] args)
{
//把线程池的最大值设置为1000
ThreadPool.SetMaxThreads(1000, 1000);
ThreadPoolMessage("Start");
ReadFile();
Console.ReadKey();
}
static void ReadFile()
{
byte[] byteData=new byte[80961024];
FileStream stream = new FileStream("File1.sour", FileMode.OpenOrCreate,
FileAccess.ReadWrite, FileShare.ReadWrite, 1024, true);
//把FileStream对象,byte[]对象,长度等有关数据绑定到FileData对象中,以附带属性方式送到回调函数
FileData fileData = new FileData();
fileData.Stream = stream;
fileData.Length = (int)stream.Length;
fileData.ByteData = byteData;
//启动异步读取
stream.BeginRead(byteData, 0, fileData.Length, new AsyncCallback(Completed), fileData);
}
static void Completed(IAsyncResult result)
{
ThreadPoolMessage("Completed");
//把AsyncResult.AsyncState转换为FileData对象,以FileStream.EndRead完成异步读取
FileData fileData = (FileData)result.AsyncState;
int length=fileData.Stream.EndRead(result);
fileData.Stream.Close();
//如果读取到的长度与输入长度不一致,则抛出异常
if (length != fileData.Length)
throw new Exception("Stream is not complete!");
string data=Encoding.ASCII.GetString(fileData.ByteData, 0, fileData.Length);
Console.WriteLine(data.Substring(2,22));
}
//显示线程池现状
static void ThreadPoolMessage(string data)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("{0}\n CurrentThreadId is {1}\n "+
"WorkerThreads is:{2} CompletionPortThreads is :{3}",
data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
注意:如果你看到的测试结果正好相反:工作者线程为999,I/O线程为1000,这是因为FileStream的文件容量小于缓冲值1024所致的。此时文件将会一次性读取或写入,而系统将启动工作者线程而非I/O线程来处理回调函数。
3.2 异步操作TCP/IP套接字
在介绍 TCP/IP 套接字前先简单介绍一下 NetworkStream 类,它是用于网络访问的基础数据流。 NetworkStream 提供了好几个方法控制套接字数据的发送与接收, 其中BeginRead、EndRead、BeginWrite、EndWrite 能够实现异步操作,而且异步线程是来自于CLR线程池的I/O线程。
public override int ReadByte ()
public override int Read (byte[] buffer,int offset, int size)
public override void WriteByte (byte value)
public override void Write (byte[] buffer,int offset, int size)
public override IAsyncResult BeginRead (byte [] buffer, int offset, int size, AsyncCallback callback, Object state )
public override int EndRead(IAsyncResult result)
public override IAsyncResult BeginWrite (byte [] buffer, int offset, int size, AsyncCallback callback, Object state )
public override void EndWrite(IAsyncResult result)
若要创建 NetworkStream,必须提供已连接的 Socket。而在.NET中使用TCP/IP套接字不需要直接与Socket打交道,因为.NET把Socket的大部分操作都放在System.Net.TcpListener和System.Net.Sockets.TcpClient里面,这两个类大大地简化了Socket的操作。一般套接字对象Socket包含一个Accept()方法,此方法能产生阻塞来等待客户端的请求,而在TcpListener类里也包含了一个相似的方法 public TcpClient AcceptTcpClient()用于等待客户端的请求。此方法将会返回一个TcpClient 对象,通过 TcpClient 的 public NetworkStream GetStream()方法就能获取NetworkStream对象,控制套接字数据的发送与接收。
下面以一个例子说明异步调用TCP/IP套接字收发数据的过程。
首先在服务器端建立默认地址127.0.0.1用于收发信息,使用此地址与端口500新建TcpListener对象,调用TcpListener.Start 侦听传入的连接请求,再使用一个死循环来监听信息。
在ChatClient类包括有接收信息与发送信息两个功能:当接收到客户端请求时,它会利用 NetworkStream.BeginRead 读取客户端信息,并在回调函数ReceiveAsyncCallback中输出信息内容,若接收到的信息的大小小于1时,它将会抛出一个异常。当信息成功接收后,再使用 NetworkStream.BeginWrite 方法回馈信息到客户端。
class Program
{
static void Main(string[] args)
{
//设置CLR线程池最大线程数
ThreadPool.SetMaxThreads(1000, 1000);
//默认地址为127.0.0.1
IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
TcpListener tcpListener = new TcpListener(ipAddress, 500);
tcpListener.Start();
//以一个死循环来实现监听
while (true)
{ //调用一个ChatClient对象来实现监听
ChatClient chatClient = new ChatClient(tcpListener.AcceptTcpClient());
}
}
}
public class ChatClient
{
static TcpClient tcpClient;
static byte[] byteMessage;
static string clientEndPoint;
public ChatClient(TcpClient tcpClient1)
{
tcpClient = tcpClient1;
byteMessage = new byte[tcpClient.ReceiveBufferSize];
//显示客户端信息
clientEndPoint = tcpClient.Client.RemoteEndPoint.ToString();
Console.WriteLine("Client's endpoint is " + clientEndPoint);
//使用NetworkStream.BeginRead异步读取信息
NetworkStream networkStream = tcpClient.GetStream();
networkStream.BeginRead(byteMessage, 0, tcpClient.ReceiveBufferSize,
new AsyncCallback(ReceiveAsyncCallback), null);
}
public void ReceiveAsyncCallback(IAsyncResult iAsyncResult)
{
//显示CLR线程池状态
Thread.Sleep(100);
ThreadPoolMessage("\nMessage is receiving");
//使用NetworkStream.EndRead结束异步读取
NetworkStream networkStreamRead = tcpClient.GetStream();
int length=networkStreamRead.EndRead(iAsyncResult);
//如果接收到的数据长度少于1则抛出异常
if (length < 1)
{
tcpClient.GetStream().Close();
throw new Exception("Disconnection!");
}
//显示接收信息
string message = Encoding.UTF8.GetString(byteMessage, 0, length);
Console.WriteLine("Message:" + message);
//使用NetworkStream.BeginWrite异步发送信息
byte[] sendMessage = Encoding.UTF8.GetBytes("Message is received!");
NetworkStream networkStreamWrite=tcpClient.GetStream();
networkStreamWrite.BeginWrite(sendMessage, 0, sendMessage.Length,
new AsyncCallback(SendAsyncCallback), null);
}
//把信息转换成二进制数据,然后发送到客户端
public void SendAsyncCallback(IAsyncResult iAsyncResult)
{
//显示CLR线程池状态
Thread.Sleep(100);
ThreadPoolMessage("\nMessage is sending");
//使用NetworkStream.EndWrite结束异步发送
tcpClient.GetStream().EndWrite(iAsyncResult);
//重新监听
tcpClient.GetStream().BeginRead(byteMessage, 0, tcpClient.ReceiveBufferSize,
new AsyncCallback(ReceiveAsyncCallback), null);
}
//显示线程池现状
static void ThreadPoolMessage(string data)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("{0}\n CurrentThreadId is {1}\n " +
"WorkerThreads is:{2} CompletionPortThreads is :{3}\n",
data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
而在客户端只是使用简单的开发方式,利用TcpClient连接到服务器端,然后调用NetworkStream.Write方法发送信息,最后调用NetworkStream.Read方法读取回馈信息。
static void Main(string[] args)
{
//连接服务端
TcpClient tcpClient = new TcpClient("127.0.0.1", 500);
//发送信息
NetworkStream networkStream = tcpClient.GetStream();
byte[] sendMessage = Encoding.UTF8.GetBytes("Client request connection!");
networkStream.Write(sendMessage, 0, sendMessage.Length);
networkStream.Flush();
//接收信息
byte[] receiveMessage=new byte[1024];
int count=networkStream.Read(receiveMessage, 0,1024);
Console.WriteLine(Encoding.UTF8.GetString(receiveMessage));
Console.ReadKey();
}
3.3 异步WebRequest
System.Net.WebRequest 是 .NET 为实现访问 Internet 的 “请求/响应模型” 而开发的一个 abstract 基类, 它主要有三个子类:FtpWebRequest、HttpWebRequest、FileWebRequest。当使用WebRequest.Create(string uri)创建对象时,应用程序就可以根据请求协议判断实现类来进行操作。FileWebRequest、FtpWebRequest、HttpWebRequest 各有其作用:FileWebRequest 使用 “file://路径” 的URI方式实现对本地资源和内部文件的请求/响应、FtpWebRequest 使用FTP文件传输协议实现文件请求/响应、HttpWebRequest 用于处理HTTP的页面请求/响应。由于使用方法相类似,下面就以常用的HttpWebRequest为例子介绍一下异步WebRequest的使用方法。
在使用ASP.NET开发网站的时候,往往会忽略了HttpWebRequest的使用,因为开发都假设客户端是使用浏览器等工具去阅读页面的。但如果你对REST开发方式有所了解,那对 HttpWebRequest 就应该非常熟悉。它可以在路径参数、头文件、页面主体、Cookie 等多处地方加入请求条件,然后对回复数据进行适当处理。HttpWebRequest 包含有以下几个常用方法用于处理请求/响应:
public override Stream GetRequestStream ()
public override WebResponse GetResponse ()
public override IAsyncResult BeginGetRequestStream ( AsyncCallback callback, Object state )
public override Stream EndGetRequestStream ( IAsyncResult asyncResult )
public override IAsyncResult BeginGetResponse ( AsyncCallback callback, Object state )
public override WebResponse EndGetResponse ( IAsyncResult asyncResult )
其中BeginGetRequestStream、EndGetRequestStream 用于异步向HttpWebRequest对象写入请求信息; BeginGetResponse、EndGetResponse 用于异步发送页面请求并获取返回信息。使用异步方式操作Internet的“请求/响应”,避免主线程长期处于等待状态,而操作期间异步线程是来自CLR线程池的I/O线程。
注意:请求与响应不能使用同步与异步混合开发模式,即当请求写入使用GetRequestStream同步模式,即使响应使用BeginGetResponse异步方法,操作也与GetRequestStream方法在于同一线程内。
下面以简单的例子介绍一下异步请求的用法。
首先为Person类加上可序列化特性,在服务器端建立Hanlder.ashx,通过Request.InputStream 获取到请求数据并把数据转化为String对象,此实例中数据是以 “Id:1” 的形式实现传送的。然后根据Id查找对应的Person对象,并把Person对象写入Response.OutStream 中返还到客户端。
在客户端先把 HttpWebRequird.Method 设置为 “post”,使用异步方式通过BeginGetRequireStream获取请求数据流,然后写入请求数据 “Id:1”。再使用异步方法BeginGetResponse 获取回复数据,最后把数据反序列化为Person对象显示出来。
注意:HttpWebRequire.Method默认为get,在写入请求前必须把HttpWebRequire.Method设置为post,否则在使用BeginGetRequireStream 获取请求数据流的时候,系统就会发出 “无法发送具有此谓词类型的内容正文” 的异常。
Model
namespace Model
{
[Serializable]
public class Person
{
public int ID
{
get;
set;
}
public string Name
{
get;
set;
}
public int Age
{
get;
set;
}
}
}
服务器端
public class Handler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
//把信息转换为String,找出输入条件Id
byte[] bytes=new byte[1024];
int length=context.Request.InputStream.Read(bytes,0,1024);
string condition = Encoding.Default.GetString(bytes);
int id = int.Parse(condition.Split(new string[] { ":" },
StringSplitOptions.RemoveEmptyEntries)[1]);
//根据Id查找对应Person对象
var person = GetPersonList().Where(x => x.ID == id).First();
//所Person格式化为二进制数据写入OutputStream
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(context.Response.OutputStream, person);
}
//模拟源数据
private IList<Person> GetPersonList()
{
var personList = new List<Person>();
var person1 = new Person();
person1.ID = 1;
person1.Name = "Leslie";
person1.Age = 30;
personList.Add(person1);
// ...........
return personList;
}
public bool IsReusable
{
get { return true;}
}
}
客户端
class Program
{
static void Main(string[] args)
{
ThreadPool.SetMaxThreads(1000, 1000);
Request();
Console.ReadKey();
}
static void Request()
{
ThreadPoolMessage("Start");
//使用WebRequest.Create方法建立HttpWebRequest对象
HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(
"http://localhost:5700/Handler.ashx");
webRequest.Method = "post";
//对写入数据的RequestStream对象进行异步请求
IAsyncResult result=webRequest.BeginGetRequestStream(
new AsyncCallback(EndGetRequestStream),webRequest);
}
static void EndGetRequestStream(IAsyncResult result)
{
ThreadPoolMessage("RequestStream Complete");
//获取RequestStream
HttpWebRequest webRequest = (HttpWebRequest)result.AsyncState;
Stream stream=webRequest.EndGetRequestStream(result);
//写入请求条件
byte[] condition = Encoding.Default.GetBytes("Id:1");
stream.Write(condition, 0, condition.Length);
//异步接收回传信息
IAsyncResult responseResult = webRequest.BeginGetResponse(
new AsyncCallback(EndGetResponse), webRequest);
}
static void EndGetResponse(IAsyncResult result)
{
//显出线程池现状
ThreadPoolMessage("GetResponse Complete");
//结束异步请求,获取结果
HttpWebRequest webRequest = (HttpWebRequest)result.AsyncState;
WebResponse webResponse = webRequest.EndGetResponse(result);
//把输出结果转化为Person对象
Stream stream = webResponse.GetResponseStream();
BinaryFormatter formatter = new BinaryFormatter();
var person=(Person)formatter.Deserialize(stream);
Console.WriteLine(string.Format("Person Id:{0} Name:{1} Age:{2}",
person.ID, person.Name, person.Age));
}
//显示线程池现状
static void ThreadPoolMessage(string data)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("{0}\n CurrentThreadId is {1}\n " +
"WorkerThreads is:{2} CompletionPortThreads is :{3}\n",
data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
3.5 异步调用WebService
相比TCP/IP套接字,在使用WebService的时候,服务器端需要更复杂的操作处理,使用时间往往会更长。为了避免客户端长期处于等待状态,在配置服务引用时选择 “生成异步操作”,系统可以自动建立异步调用的方式。
以.NET 2.0以前,系统都是使用ASMX来设计WebService,而近年来WCF可说是火热登场,下面就以WCF为例子简单介绍一下异步调用WebService的例子。
由于系统可以自动生成异步方法,使用起来非常简单,首先在服务器端建立服务ExampleService,里面包含方法Method。客户端引用此服务时,选择 “生成异步操作”。然后使用 BeginMethod 启动异步方法, 在回调函数中调用EndMethod结束异步调用。
服务端
[ServiceContract]
public interface IExampleService
{
[OperationContract]
string Method(string name);
}
public class ExampleService : IExampleService
{
public string Method(string name)
{
return "Hello " + name;
}
}
class Program
{
static void Main(string[] args)
{
ServiceHost host = new ServiceHost(typeof(ExampleService));
host.Open();
Console.ReadKey();
host.Close();
}
}
<configuration>
<system.serviceModel>
<services>
<service name="Example.ExampleService">
<endpoint address="" binding="wsHttpBinding" contract="Example.IExampleService">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
<host>
<baseAddresses>
<add baseAddress="http://localhost:7200/Example/ExampleService/" />
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
客户端
class Program
{
static void Main(string[] args)
{
//设置最大线程数
ThreadPool.SetMaxThreads(1000, 1000);
ThreadPoolMessage("Start");
//建立服务对象,异步调用服务方法
ExampleServiceReference.ExampleServiceClient exampleService = new
ExampleServiceReference.ExampleServiceClient();
exampleService.BeginMethod("Leslie",new AsyncCallback(AsyncCallbackMethod),
exampleService);
Console.ReadKey();
}
static void AsyncCallbackMethod(IAsyncResult result)
{
Thread.Sleep(1000);
ThreadPoolMessage("Complete");
ExampleServiceReference.ExampleServiceClient example =
(ExampleServiceReference.ExampleServiceClient)result.AsyncState;
string data=example.EndMethod(result);
Console.WriteLine(data);
}
//显示线程池现状
static void ThreadPoolMessage(string data)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("{0}\n CurrentThreadId is {1}\n " +
"WorkerThreads is:{2} CompletionPortThreads is :{3}\n",
data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
<configuration>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="WSHttpBinding_IExampleService" closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
bypassProxyOnLocal="false" transactionFlow="false"
hostNameComparisonMode="StrongWildcard" maxBufferPoolSize="524288"
maxReceivedMessageSize="65536" messageEncoding="Text" textEncoding="utf-8"
useDefaultWebProxy="true" allowCookies="false">
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<reliableSession ordered="true" inactivityTimeout="00:10:00" enabled="false" />
<security mode="Message">
<transport clientCredentialType="Windows" proxyCredentialType="None"
realm="" />
<message clientCredentialType="Windows" negotiateServiceCredential="true"
algorithmSuite="Default" />
</security>
</binding>
</wsHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:7200/Example/ExampleService/"
binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IExampleService"
contract="ExampleServiceReference.IExampleService"
name="WSHttpBinding_IExampleService">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
</client>
</system.serviceModel>
</configuration>
3.6 异步 SqlCommand
从ADO.NET 2.0开始,SqlCommand就新增了几个异步方法执行SQL命令。相对于同步执行方式,它使主线程不需要等待数据库的返回结果,在使用复杂性查询或批量插入时将有效提高主线程的效率。使用异步SqlCommand的时候,请注意把ConnectionString 的 Asynchronous Processing 设置为 true 。
注意:SqlCommand异步操作的特别之处在于线程并不依赖于CLR线程池,而是由Windows内部提供,这比使用异步委托更有效率。但如果需要使用回调函数的时候,回调函数的线程依然是来自于CLR线程池的工作者线程。
SqlCommand有以下几个方法支持异步操作:
public IAsyncResult BeginExecuteNonQuery (……)
public int EndExecuteNonQuery(IAsyncResult)
public IAsyncResult BeginExecuteReader(……)
public SqlDataReader EndExecuteReader(IAsyncResult)
public IAsyncResult BeginExecuteXmlReader (……)
public XmlReader EndExecuteXmlReader(IAsyncResult)
由于使用方式相似,此处就以 BeginExecuteNonQuery 为例子,介绍一下异步SqlCommand的使用。首先建立connectionString,注意把Asynchronous Processing设置为true来启动异步命令,然后把SqlCommand.CommandText设置为 WAITFOR DELAY “0:0:3” 来虚拟数据库操作。再通过BeginExecuteNonQuery启动异步操作,利用轮询方式监测操作情况。最后在操作完成后使用EndExecuteNonQuery完成异步操作。
class Program
{
//把Asynchronous Processing设置为true
static string connectionString = "Data Source=LESLIE-PC;Initial Catalog=Business;“+
"Integrated Security=True;Asynchronous Processing=true";
static void Main(string[] args)
{
//把CLR线程池最大线程数设置为1000
ThreadPool.SetMaxThreads(1000, 1000);
ThreadPoolMessage("Start");
//使用WAITFOR DELAY命令来虚拟操作
SqlConnection connection = new SqlConnection(connectionString);
SqlCommand command = new SqlCommand("WAITFOR DELAY '0:0:3';", connection);
connection.Open();
//启动异步SqlCommand操作,利用轮询方式监测操作
IAsyncResult result = command.BeginExecuteNonQuery();
ThreadPoolMessage("BeginRead");
while (!result.AsyncWaitHandle.WaitOne(500))
Console.WriteLine("Main thread do work........");
//结束异步SqlCommand
int count= command.EndExecuteNonQuery(result);
ThreadPoolMessage("\nCompleted");
Console.ReadKey();
}
//显示线程池现状
static void ThreadPoolMessage(string data)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("{0}\n CurrentThreadId is {1}\n "+
"WorkerThreads is:{2} CompletionPortThreads is :{3}\n",
data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
如果觉得使用轮询方式过于麻烦,可以使用回调函数,但要注意当调用回调函数时,线程是来自于CLR线程池的工作者线程。
class Program
{
//把Asynchronous Processing设置为true
static string connectionString = "Data Source=LESLIE-PC;Initial Catalog=Business;”+
“Integrated Security=True;Asynchronous Processing=true";
static void Main(string[] args)
{
//把CLR线程池最大线程数设置为1000
ThreadPool.SetMaxThreads(1000, 1000);
ThreadPoolMessage("Start");
//使用WAITFOR DELAY命令来虚拟操作
SqlConnection connection = new SqlConnection(connectionString);
SqlCommand command = new SqlCommand("WAITFOR DELAY '0:0:3';", connection);
connection.Open();
//启动异步SqlCommand操作,并把SqlCommand对象传递到回调函数
IAsyncResult result = command.BeginExecuteNonQuery(
new AsyncCallback(AsyncCallbackMethod),command);
Console.ReadKey();
}
static void AsyncCallbackMethod(IAsyncResult result)
{
Thread.Sleep(200);
ThreadPoolMessage("AsyncCallback");
SqlCommand command = (SqlCommand)result.AsyncState;
int count=command.EndExecuteNonQuery(result);
command.Connection.Close();
}
//显示线程池现状
static void ThreadPoolMessage(string data)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("{0}\n CurrentThreadId is {1}\n "+
"WorkerThreads is:{2} CompletionPortThreads is :{3}\n",
data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
3.7 并行编程与PLINQ
要使用多线程开发,必须非常熟悉Thread的使用,而且在开发过程中可能会面对很多未知的问题。为了简化开发,.NET 4.0 特别提供一个并行编程库System.Threading.Tasks,它可以简化并行开发,你无需直接跟线程或线程池打交道,就可以简单建立多线程应用程序。此外,.NET还提供了新的一组扩展方法PLINQ,它具有自动分析查询功能,如果并行查询能提高系统效率,则同时运行,如果查询未能从并行查询中受益,则按原顺序查询。下面将详细介绍并行操作的方式。
3.7.1 泛型委托
使用并行编程可以同时操作多个委托,在介绍并行编程前先简单介绍一下两个泛型委托System.Func<>与System.Action<>。
Func<>是一个能接受多个参数和一个返回值的泛型委托,它能接受0个到16个输入参数, 其中 T1,T2,T3,T4……T16 代表自定的输入类型,TResult为自定义的返回值。
public delegate TResult Func<TResult>()
public delegate TResult Func<T1,TResult>(T1 arg1)
public delegate TResult Func<T1,T2, TResult>(T1 arg1,T2 arg2)
public delegate TResult Func<T1,T2, T3, TResult>(T1 arg1,T2 arg2,T3 arg3)
public delegate TResult Func<T1,T2, T3, ,T4, TResult>(T1 arg1,T2 arg2,T3 arg3,T4 arg4)
..............
public delegate TResult Func<T1,T2, T3, ,T4, ...... ,T16,TResult>(T1 arg1,T2 arg2,T3 arg3,T4 arg4,...... ,T16 arg16)
Action<>与Func<>十分相似,不同在于Action<>的返回值为void,Action能接受0~16个参数
public delegate void Action<T1>()
public delegate void Action<T1,T2>(T1 arg1,T2 arg2)
public delegate void Action<T1,T2, T3>(T1 arg1,T2 arg2, T3 arg3)
.............
public delegate void Action<T1,T2, T3, ,T4, ...... ,T16>(T1 arg1,T2 arg2,T3 arg3,T4 arg4,...... ,T16 arg16)
3.7.2 任务并行库(TPL)
System.Threading.Tasks中的类被统称为任务并行库(Task Parallel Library,TPL),TPL使用CLR线程池把工作分配到CPU,并能自动处理工作分区、线程调度、取消支持、状态管理以及其他低级别的细节操作,极大地简化了多线程的开发。
注意:TPL比Thread更具智能性,当它判断任务集并没有从并行运行中受益,就会选择按顺序运行。但并非所有的项目都适合使用并行开发,创建过多并行任务可能会损害程序的性能,降低运行效率。
TPL包括常用的数据并行与任务并行两种执行方式:
3.7.2.1 数据并行
数据并行的核心类就是System.Threading.Tasks.Parallel,它包含两个静态方法 Parallel.For 与 Parallel.ForEach, 使用方式与for、foreach相仿。通过这两个方法可以并行处理System.Func<>、System.Action<>委托。
以下一个例子就是利用 public static ParallelLoopResult For( int from, int max, Action) 方法对List进行并行查询。
假设使用单线程方式查询3个Person对象,需要用时大约6秒,在使用并行方式,只需使用2秒就能完成查询,而且能够避开Thread的繁琐处理。
class Program
{
static void Main(string[] args)
{
//设置最大线程数
ThreadPool.SetMaxThreads(1000, 1000);
//并行查询
Parallel.For(0, 3,n =>
{
Thread.Sleep(2000); //模拟查询
ThreadPoolMessage(GetPersonList()[n]);
});
Console.ReadKey();
}
//模拟源数据
static IList<Person> GetPersonList()
{
var personList = new List<Person>();
var person1 = new Person();
person1.ID = 1;
person1.Name = "Leslie";
person1.Age = 30;
personList.Add(person1);
...........
return personList;
}
//显示线程池现状
static void ThreadPoolMessage(Person person)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("Person ID:{0} Name:{1} Age:{2}\n" +
" CurrentThreadId is {3}\n WorkerThreads is:{4}" +
" CompletionPortThreads is :{5}\n",
person.ID, person.Name, person.Age,
Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
若想停止操作,可以利用ParallelLoopState参数,下面以ForEach作为例子。
public static ParallelLoopResult ForEach<TSource>( IEnumerable<TSource> source,
Action<TSource, ParallelLoopState> action)
其中source为数据集,在
Action<TSource,ParallelLoopState>
委托的ParallelLoopState参数当中包含有Break()和 Stop()两个方法都可以使迭代停止。Break的使用跟传统for里面的使用方式相似,但因为处于并行处理当中,使用Break并不能保证所有运行能立即停止,在当前迭代之前的迭代会继续执行。若想立即停止操作,可以使用Stop方法,它能保证立即终止所有的操作,无论它们是处于当前迭代的之前还是之后。
class Program
{
static void Main(string[] args)
{
//设置最大线程数
ThreadPool.SetMaxThreads(1000, 1000);
//并行查询
Parallel.ForEach(GetPersonList(), (person, state) =>
{
if (person.ID == 2)
state.Stop();
ThreadPoolMessage(person);
});
Console.ReadKey();
}
//模拟源数据
static IList<Person> GetPersonList()
{
var personList = new List<Person>();
var person1 = new Person();
person1.ID = 1;
person1.Name = "Leslie";
person1.Age = 30;
personList.Add(person1);
..........
return personList;
}
//显示线程池现状
static void ThreadPoolMessage(Person person)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("Person ID:{0} Name:{1} Age:{2}\n" +
" CurrentThreadId is {3}\n WorkerThreads is:{4}" +
" CompletionPortThreads is :{5}\n",
person.ID, person.Name, person.Age,
Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
当要在多个线程中调用本地变量,可以使用以下方法:
public static ParallelLoopResult ForEach<TSource, TLocal>(
IEnumerable<Of TSource>,
Func<Of TLocal>,
Func<Of TSource,ParallelLoopState,TLocal,TLocal>,
Action<Of TLocal>)
其中第一个参数为数据集;
第二个参数是一个Func委托,用于在每个线程执行前进行初始化;
第三个参数是委托Func,它能对数据集的每个成员进行迭代,当中T1是数据集的成员,T2是一个ParallelLoopState对 象,它可以控制迭代的状态,T3是线程中的本地变量;
第四个参数是一个Action委托,用于对每个线程的最终状态进行最终操作。
在以下例子中,使用ForEach计算多个Order的总体价格。在ForEach方法中,首先把参数初始化为0f,然后用把同一个Order的多个OrderItem价格进行累加,计算出Order的价格,最后把多个Order的价格进行累加,计算出多个Order的总体价格。
public class Order
{
public int ID;
public float Price;
}
public class OrderItem
{
public int ID;
public string Goods;
public int OrderID;
public float Price;
public int Count;
}
class Program
{
static void Main(string[] args)
{
//设置最大线程数
ThreadPool.SetMaxThreads(1000, 1000);
float totalPrice = 0f;
//并行查询
var parallelResult = Parallel.ForEach(GetOrderList(),
() => 0f, //把参数初始值设为0
(order, state, orderPrice) =>
{
//计算单个Order的价格
orderPrice = GetOrderItem().Where(item => item.OrderID == order.ID)
.Sum(item => item.Price * item.Count);
order.Price = orderPrice;
ThreadPoolMessage(order);
return orderPrice;
},
(finallyPrice) =>
{
totalPrice += finallyPrice;//计算多个Order的总体价格
}
);
while (!parallelResult.IsCompleted)
Console.WriteLine("Doing Work!");
Console.WriteLine("Total Price is:" + totalPrice);
Console.ReadKey();
}
//虚拟数据
static IList<Order> GetOrderList()
{
IList<Order> orderList = new List<Order>();
Order order1 = new Order();
order1.ID = 1;
orderList.Add(order1);
............
return orderList;
}
//虚拟数据
static IList<OrderItem> GetOrderItem()
{
IList<OrderItem> itemList = new List<OrderItem>();
OrderItem orderItem1 = new OrderItem();
orderItem1.ID = 1;
orderItem1.Goods = "iPhone 4S";
orderItem1.Price = 6700;
orderItem1.Count = 2;
orderItem1.OrderID = 1;
itemList.Add(orderItem1);
...........
return itemList;
}
//显示线程池现状
static void ThreadPoolMessage(Order order)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("OrderID:{0} OrderPrice:{1}\n" +
" CurrentThreadId is {2}\n WorkerThreads is:{3}" +
" CompletionPortThreads is:{4}\n",
order.ID, order.Price,
Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
3.7.2.2 任务并行
在TPL当中还可以使用Parallel.Invoke方法触发多个异步任务,其中 actions 中可以包含多个方法或者委托,parallelOptions用于配置Parallel类的操作。
public static void Invoke(Action[] actions )
public static void Invoke(ParallelOptions parallelOptions, Action[] actions )
下面例子中利用了Parallet.Invoke并行查询多个Person,actions当中可以绑定方法、lambda表达式或者委托,注意绑定方法时必须是返回值为void的无参数方法。
class Program
{
static void Main(string[] args)
{
//设置最大线程数
ThreadPool.SetMaxThreads(1000, 1000);
//任务并行
Parallel.Invoke(option,
PersonMessage,
()=>ThreadPoolMessage(GetPersonList()[1]),
delegate(){
ThreadPoolMessage(GetPersonList()[2]);
});
Console.ReadKey();
}
static void PersonMessage()
{
ThreadPoolMessage(GetPersonList()[0]);
}
//显示线程池现状
static void ThreadPoolMessage(Person person)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("Person ID:{0} Name:{1} Age:{2}\n" +
" CurrentThreadId is {3}\n WorkerThreads is:{4}" +
" CompletionPortThreads is :{5}\n",
person.ID, person.Name, person.Age,
Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
//模拟源数据
static IList<Person> GetPersonList()
{
var personList = new List<Person>();
var person1 = new Person();
person1.ID = 1;
person1.Name = "Leslie";
person1.Age = 30;
personList.Add(person1);
..........
return personList;
}
}
3.7.3 Task简介
以Thread创建的线程被默认为前台线程,当然你可以把线程IsBackground属性设置为true,但TPL为此提供了一个更简单的类Task。
Task存在于System.Threading.Tasks命名空间当中,它可以作为异步委托的简单替代品。
通过Task的Factory属性将返回TaskFactory类,以TaskFactory.StartNew(Action)方法可以创建一个新线程,所创建的线程默认为后台线程。
class Program
{
static void Main(string[] args)
{
ThreadPool.SetMaxThreads(1000, 1000);
Task.Factory.StartNew(() => ThreadPoolMessage());
Console.ReadKey();
}
//显示线程池现状
static void ThreadPoolMessage()
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("CurrentThreadId is:{0}\n" +
"CurrentThread IsBackground:{1}\n" +
"WorkerThreads is:{2}\nCompletionPortThreads is:{3}\n",
Thread.CurrentThread.ManagedThreadId,
Thread.CurrentThread.IsBackground.ToString(),
a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
若要取消处理,可以利用CancellationTakenSource对象,在TaskFactory中包含有方法
public Task StartNew( Action action, CancellationToken cancellationToken )
在方法中加入CancellationTakenSource对象的CancellationToken属性,可以控制任务的运行,调用CancellationTakenSource.Cancel时任务就会自动停止。下面以图片下载为例子介绍一下TaskFactory的使用。
建立一个WinForm窗口,里面加入一个WebBrowser连接到服务器端的Default.aspx页面。
当按下Download按键时,系统就会利用TaskFactory.StartNew的方法建立异步线程,使用WebRequest方法向Handler.ashx发送请求。
接收到回传流时,就会根据头文件的内容判断图片的数量与每副图片的长度,把二进制数据转化为*.jpg文件保存。
系统利用TaskFactory.StartNew(action,cancellationToken) 方式异步调用GetImages方法进行图片下载。
当用户按下Cancel按钮时,异步任务就会停止。值得注意的是,在图片下载时调用了CancellationToken.ThrowIfCancellationRequested方法,目的在检查并行任务的运行情况,在并行任务被停止时释放出OperationCanceledException异常,确保用户按下Cancel按钮时,停止所有并行任务。
public partial class Form1 : Form
{
private CancellationTokenSource tokenSource = new CancellationTokenSource();
public Form1()
{
InitializeComponent();
ThreadPool.SetMaxThreads(1000, 1000);
}
private void downloadToolStripMenuItem_Click(object sender, EventArgs e)
{
Task.Factory.StartNew(GetImages,tokenSource.Token);
}
private void cancelToolStripMenuItem_Click(object sender, EventArgs e)
{
tokenSource.Cancel();
}
private void GetImages()
{
//发送请求,获取输出流
WebRequest webRequest = HttpWebRequest.Create("Http://localhost:5800/Handler.ashx");
Stream responseStream=webRequest.GetResponse().GetResponseStream();
byte[] responseByte = new byte[81960];
IAsyncResult result=responseStream.BeginRead(responseByte,0,81960,null,null);
int responseLength = responseStream.EndRead(result);
//获取图片数量
int imageCount = BitConverter.ToInt32(responseByte, 0);
//获取每副图片的长度
int[] lengths = new int[imageCount];
for (int n = 0; n < imageCount; n++)
{
int length = BitConverter.ToInt32(responseByte, (n + 1) * 4);
lengths[n] = length;
}
try
{
//保存图片
for (int n = 0; n < imageCount; n++)
{
string path = string.Format("E:/My Projects/Example/Test/Images/pic{0}.jpg", n);
FileStream file = new FileStream(path, FileMode.Create, FileAccess.ReadWrite);
//计算字节偏移量
int offset = (imageCount + 1) * 4;
for (int a = 0; a < n; a++)
offset += lengths[a];
file.Write(responseByte, offset, lengths[n]);
file.Flush();
//模拟操作
Thread.Sleep(1000);
//检测CancellationToken变化
tokenSource.Token.ThrowIfCancellationRequested();
}
}
catch (OperationCanceledException ex)
{
MessageBox.Show("Download cancel!");
}
}
}
3.7.4 并行查询(PLINQ)
并行 LINQ (PLINQ) 是 LINQ 模式的并行实现,主要区别在于 PLINQ 尝试充分利用系统中的所有处理器。 它利用所有处理器的方法,把数据源分成片段,然后在多个处理器上对单独工作线程上的每个片段并行执行查询, 在许多情况下,并行执行意味着查询运行速度显著提高。但这并不说明所有PLINQ都会使用并行方式,当系统测试要并行查询会对系统性能造成损害时,那将自动化地使用同步执行。
在System.Linq.ParallelEnumerable类中,包含了并行查询的大部分方法。
方法成员 | 说明 |
---|---|
AsParallel | PLINQ 的入口点。 指定如果可能,应并行化查询的其余部分。 |
AsSequential(Of TSource) | 指定查询的其余部分应像非并行 LINQ 查询一样按顺序运行。 |
AsOrdered | 指定 PLINQ 应保留查询的其余部分的源序列排序,直到例如通过使用 orderby(在 Visual Basic 中为 Order By)子句更改排序为止。 |
AsUnordered(Of TSource) | 指定查询的其余部分的 PLINQ 不需要保留源序列的排序。 |
WithCancellation(Of TSource) | 指定 PLINQ 应定期监视请求取消时提供的取消标记和取消执行的状态。 |
WithDegreeOfParallelism(Of TSource) | 指定 PLINQ 应当用来并行化查询的处理器的最大数目。 |
WithMergeOptions(Of TSource) | 提供有关 PLINQ 应当如何(如果可能)将并行结果合并回到使用线程上的一个序列的提示。 |
WithExecutionMode(Of TSource) | 指定 PLINQ 应当如何并行化查询(即使默认行为是按顺序运行查询)。 |
ForAll(Of TSource) | 多线程枚举方法,与循环访问查询结果不同,它允许在不首先合并回到使用者线程的情况下并行处理结果。 |
Aggregate 重载 | 对于 PLINQ 唯一的重载,它启用对线程本地分区的中间聚合以及一个用于合并所有分区结果的最终聚合函数。 |
3.7.4.1 AsParallel
通常想要实现并行查询,只需向数据源添加 AsParallel 查询操作即可。
class Program
{
static void Main(string[] args)
{
var personList=GetPersonList().AsParallel()
.Where(x=>x.Age>30);
Console.ReadKey();
}
//模拟源数据
static IList<Person> GetPersonList()
{
var personList = new List<Person>();
var person1 = new Person();
person1.ID = 1;
person1.Name = "Leslie";
person1.Age = 30;
personList.Add(person1);
...........
return personList;
}
}
3.7.4.2 AsOrdered
若要使查询结果必须保留源序列排序方式,可以使用AsOrdered方法。
AsOrdered依然使用并行方式,只是在查询过程加入额外信息,在并行结束后把查询结果再次进行排列。
class Program
{
static void Main(string[] args)
{
var personList=GetPersonList().AsParallel().AsOrdered()
.Where(x=>x.Age<30);
Console.ReadKey();
}
static IList<Person> GetPersonList()
{......}
}
3.7.4.3 WithDegreeOfParallelism
默认情况下,PLINQ 使用主机上的所有处理器,这些处理器的数量最多可达 64 个。
通过使用 WithDegreeOfParallelism(Of TSource) 方法,可以指示 PLINQ 使用不多于指定数量的处理器。
class Program
{
static void Main(string[] args)
{
var personList=GetPersonList().AsParallel().WithDegreeOfParallelism(2)
.Where(x=>x.Age<30);
Console.ReadKey();
}
static IList<Person> GetPersonList()
{.........}
}
3.7.4.4 ForAll
如果要对并行查询结果进行操作,一般会在for或foreach中执行,执行枚举操作时会使用同步方式。
有见及此,PLINQ中包含了ForAll方法,它可以使用并行方式对数据集进行操作。
class Program
{
static void Main(string[] args)
{
ThreadPool.SetMaxThreads(1000, 1000);
GetPersonList().AsParallel().ForAll(person =>{
ThreadPoolMessage(person);
});
Console.ReadKey();
}
static IList<Person> GetPersonList()
{.......}
//显示线程池现状
static void ThreadPoolMessage(Person person)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("Person ID:{0} Name:{1} Age:{2}\n" +
" CurrentThreadId is {3}\n WorkerThreads is:{4}" +
" CompletionPortThreads is :{5}\n",
person.ID, person.Name, person.Age,
Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
3.7.4.5 WithCancellation
如果需要停止查询,可以使用 WithCancellation(Of TSource) 运算符并提供 CancellationToken 实例作为参数。
与第三节Task的例子相似,如果标记上的 IsCancellationRequested 属性设置为 true,则 PLINQ 将会注意到它,并停止所有线程上的处理,然后引发 OperationCanceledException。这可以保证并行查询能够立即停止。
class Program
{
static CancellationTokenSource tokenSource = new CancellationTokenSource();
static void Main(string[] args)
{
Task.Factory.StartNew(Cancel);
try
{
GetPersonList().AsParallel().WithCancellation(tokenSource.Token)
.ForAll(person =>
{
ThreadPoolMessage(person);
});
}
catch (OperationCanceledException ex)
{ }
Console.ReadKey();
}
//在10~50毫秒内发出停止信号
static void Cancel()
{
Random random = new Random();
Thread.Sleep(random.Next(10,50));
tokenSource.Cancel();
}
static IList<Person> GetPersonList()
{......}
//显示线程池现状
static void ThreadPoolMessage(Person person)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("Person ID:{0} Name:{1} Age:{2}\n" +
" CurrentThreadId is {3}\n WorkerThreads is:{4}" +
" CompletionPortThreads is :{5}\n",
person.ID, person.Name, person.Age,
Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
}