C#网络编程高级篇之网页游戏辅助程序设计(txt+pdf+epub+mobi电子书下载)


发布时间:2020-07-21 04:22:06

点击下载

作者:张慧斌

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

C#网络编程高级篇之网页游戏辅助程序设计

C#网络编程高级篇之网页游戏辅助程序设计试读:

前言

在常年的程序设计语言教学实践中,笔者发现,已经具备一定编程基础的学生,如果没有经历过大一点的系统项目开发,其编程水平很难再有提高;但是开发系统项目的机会对初学者来说并不是容易获得的,因此笔者在程序设计教学中尝试引进游戏的辅助程序设计,以替代系统项目开发的实践。

游戏辅助程序设计技术对初学者来说是非常神秘的,却又是非常感兴趣的编程技术。游戏辅助程序设计技术是门高深的编程技术,很多初学者就是在从玩游戏、修改游戏、编写游戏程序到设计游戏的辅助程序的过程中逐步成长起来的,最后成为程序设计高手。

笔者在教学的课后作业中让学生进行游戏辅助程序设计,让他们边玩游戏边开发游戏辅助程序,这样极大地提高了他们的编程兴趣和积极性,同时大大提高了他们的编程水平,教学效果非常明显。相对来说,能达到比开发系统项目更好的效果。

现在的网络游戏以网页游戏为主,这种游戏非常流行、火爆。网页游戏是基于Internet上客户—服务器模式的,其中服务端程序运行在游戏服务器上,玩家可以通过运行客户端程序同时登录到游戏中。既然在网络游戏中有了服务器这个概念,我们以前传统的修改游戏的方法就显得无能为力了。在单机版的游戏中,可以随心所欲地通过内存搜索来修改角色的各种属性,这种方法在网络游戏中就没有任何用处了。因为在网络游戏中的角色的各种属性及各种重要资料都存放在服务器上,在玩家的机器上(客户端)只是显示角色的状态,所以通过修改客户端内存里有关角色的各种属性是不切实际的。

我们知道Internet客户—服务器模式的通信一般采用TCP/IP通信协议,其数据交换是通过IP数据包的传输来实现的;一般来说,客户端向服务器发出的某些请求,比如移动、战斗等指令,都是通过封包的形式和服务器交换数据的。那么,我们把本地发出消息称为Send,意思就是发送数据,服务器收到Send的消息后,会按照既定的程序把有关的信息反馈给客户端,比如移动的坐标、战斗的类型;把客户端收到服务器发来的有关消息称为Receive。知道了这个道理,接下来我们要做的工作就是分析客户端和服务器之间往来的数据(也就是封包),这样就可以提取到对我们有用的数据并进行修改,然后模拟服务器发给客户端,或者模拟客户端发送给服务器,从而达到修改游戏的目的了。

我们把上述对游戏数据包的修改程序称为网络游戏辅助程序。这种辅助程序就是网络游戏封包,一般在设计时都是针对某个游戏的某个版本来做的,针对性很强。因为每个网络游戏服务器与客户端交流的数据包各不相同,辅助程序必须对网络游戏服务器的数据包进行分析,才能产生服务器识别的数据包。这类程序也是当前最流行的一类游戏辅助程序。辅助程序可以协助玩家自动产生游戏动作、修改游戏网络数据包以及修改游戏内存数据等,以便玩家用最少的时间和金钱去完成功力升级、过关斩将。

Microsoft.NET是一种面向网络支持各种用户终端的开发平台环境;微软的目标是让Microsoft.NET彻底改变软件的开发方式、发行方式和使用方式等,并且不只针对微软一家,而是面向所有开发商与运营商。.NET的核心内容之一就是要搭建第三代互联网平台,这个网络平台将解决网站之间的协同合作问题,从而最大限度地获取信息。在.NET平台上,不同网站之间通过相关的协定联系在一起,网站之间形成自动交流,协同工作,提供最全面的服务。

C#是一种面向Internet和企业级应用的新一代编程语言,它是专门为.NET设计的,是一种安全、稳定、简单、优雅的,由C和C++衍生出来的面向对象的编程语言。C#在继承C和C++强大功能的同时去掉了一些复杂特性,它以其强大的操作能力、优雅的语法风格、创新的语言特性和便捷的面向组件编程而成为.NET开发的首选语言,并且成为ECMA与ISO的标准规范。

开发网页游戏的辅助程序属于网络编程技术,因此采用.NET的组件之一C#语言来设计网络程序,本书的网页游戏辅助程序就是用C#编写的。网络游戏辅助程序具有很高的技术含量,在这些程序中使用了许多高端技术,如拦截Socket技术、拦截API技术、模拟键盘和鼠标技术、直接修改程序内存技术,等等。本书将对常见的游戏辅助程序的封包技术进行全面的剖析。

本书以实例与原理相结合的方式讲解编程开发思路向每一位有志于从事网络编程开发的读者介绍这种热门的程序开发技术。

本书的所有程序均在Microsoft Visual Studio 2008简体中文版开发环境下调试通过,程序本身没有错误,但是由于提供网络游戏的商家可能不断升级游戏、变换密码系统、使用防外挂技术,等等,因此不能保证读者拿到的程序任何时刻都能起到辅助游戏的功能,如果不能起到网页游戏辅助的作用,读者可按照辅助程序设计的思路和办法,对辅助程序重新进行小幅修改,或者重新设计解码系统。虽然网页游戏可能不断变换,但是设计网页游戏辅助程序的方法和总体思路是不会变的,注重设计辅助程序的方法和思路,才能不断升级辅助程序。

游戏辅助程序设计是一个验证对某些计算机知识的理解程度的方法,是发现问题、解决问题的机会,它只能起到提高学习计算机编程的兴趣的作用,而决不是学习计算机编程的万能钥匙。

由于C#网络编程辅助程序设计技术所涉及的知识面极为广泛,而笔者的知识面又很有限,因此尽管笔者对本书中所涉及的内容一再推敲和仔细研究,仍有可能出现错误和纰漏,希望广大读者批评指正。

本书相关的代码资料可在电子工业出版社官网“在线资源”中下载。笔者第1章多线程编程技术

网络应用程序一般都会或多或少地使用到线程,甚至可以说,一个功能稍微强大的网络应用程序总会在其中开出或多或少的线程,如果应用程序中开出的线程数目大于两个,那么就可以把这个程序称为多线程应用程序。

为什么网络应用程序总会和线程交缠在一起呢?这是因为网络应用程序在执行时,会遇到很多意想不到的问题,其中最常见的是网络阻塞和网络等待。程序在处理这些问题时往往需要花费很多的时间,如果不使用线程,则程序在执行时就会表现出如运行速度慢,执行时间长,容易出现错误,反应迟钝等问题。而如果把这些可能造成大量占用程序执行时间的过程放在线程中处理,往往能够大大提高应用程序的运行效率和性能,以及获得更优良的可伸缩性。那么这是否就意味着应该在网络应用程序中广泛地使用线程呢?情况并非如此,线程其实是一把双刃剑,如果不分场合,在不需要使用的地方强行使用就可能会产生许多程序垃圾,或者在程序结束后,由于没有能够销毁创建的进程而导致应用程序挂起等问题。所以如果程序的代码运行足够快,那还是别使用线程或多线程。

本章介绍Windows下的多线程编程技术及其执行原理和机制,应用大量的示例和程序讲解进程、线程和线程池的概念,希望读者可以尽快掌握多线程编程技术。1.1 进程和线程的概念

进程是对一段静态指令序列(程序)的动态执行过程,是系统进行资源分配和调度的一个基本单位。与进程相关的信息包括进程的用户标志、正在执行的已经编译好的程序、进程程序和数据在存储器中的位置,等等。进程是程序文件的内存实例。Windows是一个多任务的系统,如果使用的是Windows2000及其以上版本,则可以通过任务管理器查看当前系统运行的程序和进程,如图1-1所示。当一个程序开始运行时,它就是一个进程,进程所指的是运行中的程序使用到的内存和系统资源。通俗点说,就是指运行中的程序,程序是“死”的,一旦运行进入内存,就“活”了,即程序的内存实例——进程。

同一进程又可以划分为若干个独立的执行流,我们称之为线程。线程是CPU调度和分配的基本单位。线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。在Windows环境下,用户可以同时运行多个应用程序,每个执行的应用程序就是一个进程。例如,浏览器就是一个很好的多线程的例子,在浏览器中用户可以在下载Java应用程序或图像的同时滚动页面,在访问新页面时,播放动画和声音,打印文件等。同时打开两个QQ时,每个运行的QQ就是一个进程;而用一个QQ和多个人聊天时,每个聊天窗口就是一个线程。图1-1 Windows系统的进程

进程和线程概念的提出,对提高软件的并行性有着重要的意义。并行性的主要特点就是并发处理。当然,对一个单处理器的计算机系统来说,单个CPU在任何时刻只能执行一个线程。在一个单处理器系统中,操作系统可以通过分时处理来获得并发,在这种情况下,系统为每个线程分配一个CPU时间片,每个线程只有在分配的时间片内才拥有对CPU的控制权,其他时间都在等待。即同一时间只有一个线程在运行。由于系统为每个线程划分的时间片很小(20毫秒左右),所以在用户看来,好像是多个线程在同时运行。实际上,一个线程执行了一个时间单元后,另外一个线程接着执行下一个时间单元,如此反复,这就是并发。

在网络环境下为什么要使用多线程呢?考虑这样一种情况:在C/S模式下,服务器需要不断监听来自各个客户端的请求,这时,如果采用单线程机制的话,服务器将无法处理其他事情,因为这个线程要不断地循环监听请求而无暇对其他请求做出响应。实际上,当要花费大量时间进行连续的操作时,或者等待网络或其他I/O设备响应时,都可以使用多线程技术。

下面,我们引入两个例子,以便更直观地看到多线程编程技术的好处。【例1.1】单线程例子,要求:单击【开始】,在左边的ListBok控件中添加10000个项,为Item n(n=0,1,2,…,99999),单击【查看】弹出对话框,显示ListBox里面的项的总数,如图1-2所示。图1-2 单线程例子程序

例1.1的代码如下所示。using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; namespace ThreadEx101 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnStart_Click(object sender, EventArgs e) { for (int index = 0; index < 100000; index++) { this.lstTest.Items.Add(string.Format("Item {0}", index)); } } private void btnLook_Click(object sender, EventArgs e) { MessageBox.Show(string.Format("ListBox中一共有{0}项", this.lstTest.Items.Count)); } } }

运行上述程序,会发现两个问题:其一,由于添加的项比较多,要等待一段时间(具体时间由计算机的配置情况来确定),显示的项是添加完成后才全部显示出来的,而不是一个个显示出来的;其二,在ListBox项显示出来之前,单击【查看】会发现不起作用,因为在单线程中,添加项的线程还没有完成,独占了整个进程的资源,Item的个数一直在自增,是不确定的,“查看”这个线程任务只能等待“开始”线程任务结束后才能运行。

在下面的例1.2中,把例1.1的单线程程序改为多线程程序,解决了上述两个问题。【例1.2】多线程例子,要求:单击【开始】,在左边的ListBok控件中添加10000个项,为Item n(n=0,1,2,…,99999),单击【查看】弹出对话框,显示ListBox里面的项的总数。

本例实现过程如下:添加1个新窗体Form2,在窗体Form2上添加和Form1同样的控件,如图1.3所示。图1-3 多线程程序界面

Form2.cs的代码如下所示。using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Threading; //引入多线程命名空间 namespace ThreadEx101 { public partial class Form2 : Form { public Form2() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { ThreadStart threadStart = new ThreadStart(AddItem); Thread thread = new Thread(threadStart); //声明一个线程 thread.Start(); //启动线程 } private void AddItem() { for (int index = 0; index < 100000; index++) { this.lstTest.Items.Add(string.Format("Item {0}", index)); } } private void button2_Click(object sender, EventArgs e) { MessageBox.Show(string.Format("ListBox中一共有{0}项", this.lstTest.Items.Count)); } } }

在主程序Program.cs中将启动窗体改为Form2,代码如下所示。using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; namespace ThreadEx101 { static class Program { //

//应用程序的主入口点 // [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form2()); //此处把原来的Form1改为Form2 } } }

注意此时运行时不要按【调试】下的【启动调试(S)】,而是按【开始执行(不调试)】来运行程序。

例1.2的运行结果和例1.1的运行结果不一样,Item n是动态一个个加上去的,而不是全部在内存中添加完成后显示出来。而且,随时可以单击【查看】,显示ListBox中的项数,运行结果如图1-4所示。图1-4 多线程程序运行结果

例1.2的运行结果解决了例1.1出现的两个问题,原因就是例1.1是单线程运行的,而例1.2是多线程运行的,ListBox中显示的任务在另一个线程中运行,“查看”任务可以同时运行。1.2 C#中进程和线程的管理1.2.1 管理进程的Process类

System.Diagnostics名称空间下的Process类专门用于完成系统的进程的管理任务。Process类可以在本地计算机上启动和停止进程,也可以查询进程的相关信息,还可以向进程查询特定类型的信息,包括加载模块的名称、启动的时间、消耗的句柄数和线程等。在自己的程序中运行其他的应用程序,实际上就是对进程进行管理。如果希望在进程中启动和停止其他进程,首先要创建Process类的实例,并设置对象的StartInfo属性,然后调用该对象的Start方法启动进程。【例1.3】启动、停止和观察进程。

本例的实现过程为:新建一个名为ProcessEx103的Windows窗体应用程序,设计界面如图1-5所示,展开工具箱中的【组件】选项卡,然后将Process组件拖放到设计窗体中。图1-5 进程管理程序界面

例1.3代码如下所示。using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Diagnostics; //添加的命名空间 using System.Threading; namespace ProcessEx103 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { process1.StartInfo.FileName = "notepad.exe"; process1.Start(); //启动Notepad.exe进程 } private void btnStop_Click(object sender, EventArgs e) { //创建新的Process组件的数组,并将它们与指定的进程名称(Notepad)的所有进程资源相关联 Process[] myprocesses; myprocesses = Process.GetProcessesByName("Notepad"); foreach (Process instance in myprocesses) { //设置终止当前线程前等待1000毫秒 instance.WaitForExit(1000); instance.CloseMainWindow(); } } private void btnWatch_Click(object sender, EventArgs e) { //创建Process类型的数组,并将它们与系统内所有进程相关联 listBox1.Items.Clear(); Process[] processes; processes = Process.GetProcesses(); foreach (Process p in processes) { //将每个进程名加入listBox1中 this.listBox1.Items.Add(p.ProcessName); } } } }

运行程序,单击几次【启动记事本】,观察打开的每个进程,然后单击【停止记事本】,观察依次停止进程的情况。

由于安装Windows操作系统后,notepad.exe就已经安装到系统文件夹下,而且在任何一个文件夹中均可直接运行,所以在这个例子中,我们选择了调用notepad.exe作为演示的例子。实际上,任何一个可执行文件均可以通过这种方法调用,读者可以自行尝试调用其他可执行文件,并观察执行效果。1.2.2 C#多线程

Visual C#中使用的线程都是通过自命名空间System.Threading中的Thread类来实例化完成的。通过Thread类的构造函数来创建可供Visual C#使用的线程,通过Thread中的方法和属性来设定线程属性和控制线程的状态。

Thread还提供了其他的构造函数来创建线程,这里就不一一介绍了。表1-1是Thread类中的一些常用的方法及其简要说明。表1-1 Thread类的常用属性及其说明

1. 启动线程

在使用线程前,首先要创建一个线程,其一般形式为:Thread t=new Thread(enterPoint);

其中enterPoint为线程的入口,即线程开始执行的方法。在托管代码中,通过委托来处理线程执行的代码,例如:Thread t=new Thread(new ThreadStart(methodName));

创建线程实例后,就可以调用Start方法来启动线程了。

2. 终止线程

线程启动后,当不需要某个线程继续执行时,有两种终止线程的方法。

第一种是事先设置一个布尔变量,在其他线程中通过修改该变量的值作为传递给该线程是否需要终止的判断条件,在该线程中循环判断该条件,以确定是否退出线程,这是结束线程的比较好的方法,在实际编程中一般使用这种方法。

第二种方法是通过调用Thread类的Abort方法来强行终止线程。例如,t.Abort()。Abort方法没有任何参数,线程一旦被终止,就无法再重新启动。由于Abort通过抛出异常来强行终止线程,因此在实际编程中,应该尽量避免采用这种方法。

调用Abort方法终止线程时,公共语言运行库(CLR)会引发ThreadAbortException异常,程序员可以在线程中捕获ThreadAbortException异常,然后在异常处理的Catch块或者Finally块中进行释放资源等代码处理工作。但是,线程中也可以不捕获ThreadAbort-Exception异常,而由系统自动进行释放资源等处理工作。

注意,如果线程中捕获了ThreadAbortException异常,系统在Finally子句的结尾处会再次引发ThreadAbortException异常,如果没有Finally子句,则会在Catch子句的结尾处再次引发该异常。为了避免再次引发异常,可以在Finally子句的结尾处或者Catch子句的结尾处调用System.Threading.Thread.ResetAbort方法来防止系统再次引发该异常。

使用Abort方法终止线程,调用Abort方法后,线程不一定会立即结束。这是因为系统在结束线程前要进行代码清理等工作,这种机制可以使线程的终止比较安全,但清理代码需要一定的时间,而我们并不知道这个工作将需要多长时间。因此,调用了线程的Abort方法后,如果系统自动清理代码的工作没有结束,可能会出现类似死机一样的假象。为了解决这个问题,可以在主线程中调用子线程对象的Join方法,并在Join方法中指定主线程等待子线程结束的等待时间。

3. 合并线程

Join方法用于把两个并行执行的线程合并为一个单个的线程。如果一个线程t1在执行的过程中需要等待另一个线程t2结束后才能继续执行,可以在t1的程序模块中调用t2的join()方法。例如,t2.Join()。

这样t1在执行到t2.Join()语句后就会处于阻塞状态,直到t2结束后才会继续执行。

但是假如t2一直不结束,那么等待就没有意义了。为了解决这个问题,可以在调用t2的Join方法时指定一个等待时间,这样t1这个线程就不会一直等待下去。例如,如果希望将t2合并到t1后,t1只等待100毫秒,然后不论t2是否结束,t1都继续执行,就可以在t1中加上语句:t2.Join(100)。Join方法通常和Abort一起使用。

由于调用某个线程的Abort方法后,我们无法确定系统清理代码的工作什么时候才能结束,因此如果希望主线程调用了子线程的Abort方法后,主线程不必一直等待,可以调用子线程的Join方法将子线程连接到主线程中,并在连接方法中指定一个最大等待时间,这样就能使主线程继续执行。

4. 让线程休眠

在多线程应用程序中,有时候并不希望某一个线程继续执行,而是希望该线程暂停一段时间,等待其他线程执行之后再继续执行。这时可以调用Thread类的Sleep方法,即让线程休眠。例如,Thread.Sleep(1000)。这条语句的功能是让当前线程休眠1000毫秒。

注意,调用Sleep方法的是类本身,而不是类的实例。休眠的是该语句所在的线程,而不是其他线程。

5. 线程优先级

当线程之间争夺CPU时间片时,CPU是按照线程的优先级进行服务的。在C#应用程序中,可以对线程设定五个不同的优先级,由高到低分别是Highest、AboveNormal、Normal、BelowNormal和Lowest。在创建线程时如果不指定其优先级,则系统默认为Normal。假如想让一些重要的线程优先执行,可以使用下面的方法为其赋予较高的优先级:Thread t=new Thread(new ThreadStart(enterpoint)); t.priority=ThreadPriority.AboveNormal;

通过设置线程的优先级可以改变线程的执行顺序,所设置的优先级仅仅适用于这些线程所属的进程。

注意,当把某线程的优先级设置为Highest时,系统上正在运行的其他线程都会终止,所以使用这个优先级别时要特别小心。

6. 同步

同步是多线程中一个非常重要的概念。所谓同步,是指多个线程之间存在先后执行顺序的关联关系。如果一个线程必须在另一个线程完成某个工作后才能继续执行,则必须考虑如何让其保持同步,以确保在系统上同时运行多个线程而不会出现逻辑错误。

当两个线程t1和t2有相同的优先级,并且同时在系统上运行时,如果先把时间片分给t1使用,它在变量variable1中写入某个值,但如果在时间片用完时它仍没有完成写入,这时由于时间片已经分给t2使用,而t2又恰好要尝试读取该变量,它可能就会读出错误的值。这时,如果使用同步仅允许一个线程使用variable1,在该线程完成对variable1的写入工作后再让t2读取这个值,就可以避免出现此类错误。

为了对线程中的同步对象进行操作,C#提供了lock语句来锁定需要同步的对象。Lock关键字确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻塞),直到该对象被释放。比如线程t1对variable1操作时,为了避免其他线程也对其进行操作,可以使用lock语句锁定variable1,实现代码为:Lock(variable1) { variable1++; }

注意,锁定的对象一定要声明为private,不要锁定public类型的对象,否则将会使lock语句无法控制,从而引发一系列问题。

另外还要注意,由于锁定一个对象之后,其他任何线程都不能访问这个对象,需要使用该对象的线程就只能等待该对象被解除锁定后才能使用。因此如果在锁定和解锁期间处理的对象过多,就会降低应用程序的性能。

还有,如果两个不同的线程同时锁定两个不同的变量,而每个线程又都希望在锁定期间访问对方锁定的变量,那么两个线程在得到对方变量的访问权之前都不会释放自己锁定的对象,从而产生死锁。在编写程序时,要注意避免这类操作引起的问题。【例1.4】线程的基本用法。

新建一个名为ThreadEx104的Windows窗体应用程序,界面设计如图1-6所示,向窗体中拖放1个Timer组件和1个RichTextBox组件。图1-6 线程用法程序界面

例1.4代码如下所示。using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Threading; //添加命名空间引用 namespace ThreadEx104 { public partial class Form1 : Form { StringBuilder sb = new StringBuilder(); Thread thread1; Thread thread2; private void AppendString(string s) { lock (sb) { sb.Append(s); } } public void Method1() { while (true) { Thread.Sleep(100); //线程休眠100毫秒 AppendString("a"); } } public void Method2() { while (true) { Thread.Sleep(100); //线程休眠100毫秒 AppendString("b"); } } public Form1() { InitializeComponent(); } private void btnStart_Click(object sender, EventArgs e) { sb.Remove(0, sb.Length); timer1.Enabled = true; thread1 = new Thread(new ThreadStart(Method1)); thread2 = new Thread(new ThreadStart(Method2)); thread1.Start(); thread2.Start(); } private void btnStop_Click(object sender, EventArgs e) { thread1.Abort(); thread1.Join(10); thread2.Abort(); thread2.Join(10); } private void timer1_Tick(object sender, EventArgs e) { if (thread1.IsAlive == true || thread2.IsAlive == true) { richTextBox1.Text = sb.ToString(); } else { timer1.Enabled = false; } } } }

运行程序,单击【启动线程】后,再单击【终止线程】,从图1-7所示的运行结果中可以看到,两个具有相同优先级的线程同时执行时,在richTextBox1中添加的字符个数基本上相同。图1-7 线程用法程序结果界面1.2.3 线程池

线程池是一种多线程处理形式,为了提高系统性能,在许多地方都要用到线程池技术。例如,在一个C/S模式的应用程序中的服务器端,如果每遇到一个请求就创建一个新线程,然后在新线程中为其请求服务的话,将不可避免地造成系统开销的增大。实际上,创建太多的线程可能会导致由于过度使用系统资源而耗尽内存。为了防止资源不足,服务器端应用程序应采取一定的办法来限制同一时刻处理的线程数目。

线程池为线程生命周期的开销问题和资源不足问题提供了很好的解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,由于请求到达时线程已经存在,所以无意中也就消除了线程创建所带来的延迟。这样,就可以立即为新线程请求服务,使应用程序响应更快。而且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过了规定的最大数目时,就强制其他任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。

线程池适用于需要多个线程而实际执行时间又不多的场合,比如有些常处于阻塞状态的线程。当一个应用程序服务器接受大量短小线程的请求时,使用线程池技术是非常合适的,它可以大大减少线程创建和销毁的次数,从而提高服务器的工作效率。但是如果线程要求运行的时间比较长的话,那么此时线程的运行时间比线程的创建时间要长得多,仅靠减少线程的创建时间对系统效率的提高就不是那么明显了,此时就不适合使用线程池技术,而需要借助其他的技术来提高服务器的服务效率。

池(Pool)是一个很常见的提高性能的方式,比如线程池连接池等,之所以有这些池是因为线程和数据库连接的创建和关闭是一种比较昂贵的行为。对于这种昂贵的资源,我们往往会考虑在一个池容器中放置一些资源,在用的时候去拿,在不够的时候添加,在用完时就归还,这样就可以避免不断地创建资源和销毁资源。

当应用程序初始化时,其线程池中不含有线程。当应用程序要创建线程来执行任务时,该应用程序应请求线程池线程来执行任务。线程池知道后将创建一个初始线程。该新线程经历的初始化和其他线程一样;但是任务完成后,该线程不会自行销毁。相反,它会以挂起状态返回线程池。如果应用程序再次向线程池发出请求,那么这个挂起的线程将激活和执行任务,而不会创建新线程,这节约了很多开销。只要线程池中应用程序任务的排队速度低于一个线程处理每项任务的速度,那么就可以反复重用同一线程,从而在应用程序生存期内节约大量开销。

如果线程池中应用程序任务排队的速度超过一个线程处理任务的速度,则线程池将创建额外的线程。当然,创建新线程确实会产生额外开销,但应用程序在其生存期中很可能只请求几个线程来处理交给它的所有任务。因此,总体来说,通过使用线程池可以提高应用程序的性能。线程池的一个绝妙特性是:它是启发式的。如果应用程序需要执行很多任务,那么线程池将创建更多的线程。如果应用程序的工作负载逐渐减少,那么线程池线程将自行终止。线程池的算法确保它仅包含置于其上的工作负荷所需要的线程数。

线程池主要提供四种功能。

1. 异步调用方法

在应用程序中,如果有创建的新线程来执行任务的代码,那么用命令线程池执行该任务的新代码来替换它。事实上,让线程池执行任务比让一个新的专用线程来执行任务更容易。要排队线程池任务,可以使用System.Threading命名空间中定义的ThreadPool类。ThreadPool类只提供静态方法,且不能构造它的实例。要让线程池线程异步调用方法,代码必须调用一个ThreadPool的重载QueueUserWorkItem方法,如下所示。public static Boolean QueueUserWorkItem(WaitCallback wc, Object state);public static Boolean QueueUserWorkItem(WaitCallback wc);

这些方法将“工作项”和可选状态数据排队到线程池的线程中,并立即返回。工作项只是一种方法(由wc参数标识),它被调用后传递给单个参数,即状态(状态数据)。没有状态参数的QueueUserWorkItem版本将null传递给回调方法。最后,池中的某些线程将调用方法来处理该工作项。编写的回调方法必须与System.Threading.WaitCallback委托类型相匹配,其定义如下:public delegate void WaitCallback(Object state); 【例1.5】线程池的异步调用方法。

新建一个名为ThreadPoolEx105的Console控制台应用程序,代码如下所示。using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace ThreadPoolEx105 { class Program { //存放要计算的数值的字段 static double number1 = -1; static double number2 = -1; static void Main(string[] args) { //获取线程池的最大线程数和维护的最小空闲线程数 int maxThreadNum, portThreadNum; int minThreadNum; ThreadPool.GetMaxThreads(out maxThreadNum, out portThreadNum); ThreadPool.GetMinThreads(out minThreadNum, out portThreadNum); Console.WriteLine("最大线程数:{0}", maxThreadNum); Console.WriteLine("最小空闲线程数:{0}", minThreadNum); //函数变量值 int x = 2; //启动第一个任务:计算x的8次方 Console.WriteLine("启动第一个任务:计算{0}的8次方。", x); ThreadPool.QueueUserWorkItem(new WaitCallback(TaskProc1), x); //启动第二个任务:计算x的8次方根 Console.WriteLine("启动第二个任务:计算{0}的8次方根。", x); ThreadPool.QueueUserWorkItem(new WaitCallback(TaskProc2), x); //等待,直到两个数值都完成计算 while (number1 == -1 || number2 == -1) ; //打印计算结果 Console.WriteLine("y({0}) = {1}", x, number1 + number2); Console.ReadLine(); } //启动第一个任务:计算x的8次方 static void TaskProc1(object o) { number1 = Math.Pow(Convert.ToDouble(o), 8); } //启动第二个任务:计算x的8次方根 static void TaskProc2(object o) { number2 = Math.Pow(Convert.ToDouble(o), 1.0 / 8.0); } } }

运行结果如图1-8所示。图1-8 例1.5程序运行结果

2. 以一定的时间间隔调用方法

如果应用程序需要在某一时间执行某项任务,或者应用程序需要定期执行某些方法,那么使用线程池将是最佳的选择。System.Threading命名空间定义Timer类。当构造Timer类的实例时,是在告诉线程池想在将来的某个特定时间回调自己的某个方法。Timer类有四种构造函数:

public Timer(TimerCallback callback,Object state,Int32 dueTime,Int32 period);

public Timer(TimerCallback callback,Object state,UInt32 dueTime,UInt32 period);

public Timer(TimerCallback callback,Object state,Int64 dueTime,Int64 period);

public Timer(TimerCallback callback,Object state,Timespan dueTime,TimeSpan period)。

这四种构造函数构造完全相同的Timer对象。

回调参数标识是线程池中线程回调的方法。当然,编写的回调方法必须与System. Threading.TimerCallback委托类型相匹配,其定义如下:public delegate void TimerCallback(Object state);

构造Timer对象后,线程池知道要做什么,并自动监视时间。然而,Timer类还提供了几种其他的方法,允许与线程池进行通信,以便更改什么时候(或者是否)应当回调方法。具体地说,Timer类提供了以下六种Change和Dispose的方法:

public Boolean Change(Int32dueTime,Int32period);

public Boolean Change(UInt32 dueTime,UInt32 period);

public Boolean Change(Int64dueTime,Int64period);

public Boolean Change(TimeSpan dueTime,TimeSpan period);

public Boolean Dispose();

public Boolean Dispose(WaitHandle notifyObject)。

Change方法允许更改Timer对象的dueTime和period。Dispose方法允许在所有挂起的回调已经完成时,完全取消回调,并可选地用信号通知由notifyObject参数标识的内核对象。【例1.6】线程池线程立即调用一个方法,并且每隔两秒再次调用。

新建一个Thread PoolEx106的Console控制台应用程序,代码如下所示。using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace ThreadPoolEx106 { class Program { static void Main(string[] args) { Console.WriteLine("Checking for status updates every 2 seconds."); Console.WriteLine(" (Hit Enter to terminate the sample)"); Timer timer = new Timer(new TimerCallback(CheckStatus), null, 0, 2000); Console.ReadLine(); } static void CheckStatus(Object state) { Console.WriteLine("Checking Status."); } } }

程序结果如图1-9所示。图1-9 例1.6程序运行结果

3. 当单个内核对象得到信号通知时调用方法

许多应用程序生成线程,只是为了等待某单个内核对象得到信号通知。一旦该对象得到信号通知,这个线程就将某种通知发送给另一个线程,然后环回,等待该对象再次发出信号。有些开发人员编写的代码中甚至有几个线程,而每个线程都在等待一个对象。这是对系统资源的巨大浪费。因此,如果应用程序中有多个线程在等待单个内核对象得到信号通知,那么线程池仍将是提高应用程序性能的最佳资源。

要让线程池线程在内核对象得到信号通知时回调方法,可以再次利用System. Threading.ThreadPool类中定义的一些静态方法。要让线程池线程在内核对象得到信号通知时调用方法,代码必须调用一个重载的RegisterWaitHandle方法,代码如下所示。public static RegisterWaitHandle RegisterWaitForSingleObject (WaitHandle h, WaitOrTimerCallback callback, Object state, UInt32 milliseconds, Boolean executeOnlyOnce); public static RegisterWaitHandle RegisterWaitForSingleObject (WaitHandle h, WaitOrTimerCallback callback, Object state, Int32 milliseconds, Boolean executeOnlyOnce); public static RegisterWaitHandle RegisterWaitForSingleObject (WaitHandle h, WaitOrTimerCallback callback, Object state, TimeSpan milliseconds, Boolean executeOnlyOnce); public static RegisterWaitHandle RegisterWaitForSingleObject (WaitHandle h, WaitOrTimerCallback callback, Object state,Int64 milliseconds, Boolean executeOnlyOnce);

当调用这些方法之一时,h参数标识出想要线程池等待的内核对象。由于该参数是抽象基类System.Threading.WaitHandle,因此可以指定从该基类派生出来的任何类。特别是可以将一个引用传递给AutoResetEvent、ManualResetEvent或Mutex object。第二个参数callback标识出想要线程池线程调用的方法。实现的回调方法必须与System.Threading. WaitOrTimerCallback委托类型相匹配,其定义如下所示。public delegate void WaitOrTimerCallback(Object state, Boolean timedOut);

第三个参数state允许指定应传递给回调方法的某些状态数据,如果没有特别的状态数据要传递,则传递null。第四个参数milliseconds允许告诉线程池内核对象得到信号通知前应该等待的时间。这里通常传递—1,以表示无限超时。如果最后一个参数execute OnlyOnce为真,那么线程池线程将仅执行一次回调方法。但是,如果executeOnlyOnce为假,那么线程池线程将在内核对象每次得到信号通知时执行回调方法。这对AutoReset Event对象非常有用。

当调用回调方法时,会传递给它状态数据和Boolean值timedOut。如果timedOut为假,则该方法知道它被调用的原因是内核对象得到信号通知。如果timedOut为真,则该方法知道它被调用的原因是内核对象在指定时间内没有得到信号通知。回调方法应该执行所有必需的操作。

在前面所示的原型中,RegisterWaitForSingleObject方法返回了一个RegisteredWait-Handle对象。该对象确定线程池在等待的内核对象。如果由于某种原因,应用程序要告诉线程池停止监视已注册的等待句柄,那么应用程序就可以调用RegisteredWaitHandle的Unregister方法:public Boolean Unregister(WaitHandle waitObject);

WaitObject参数表明当执行完队列中的所有工作项后,如何得到信号通知。如果不想得到信号通知,那么应将null传递给该参数。如果将一个有效引用传递给WaitHandle-derived对象,那么线程池会在已注册等待句柄的所有挂起工作项执行完后,通知该对象。【例1.7】线程池线程在内核对象得到信号通知时调用方法的例子。

新建一个Thread PoolEx107的Console控制台应用程序,代码如下所示。using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace ThreadPoolEx107 { class Program { static void Main(string[] args) { AutoResetEvent are = new AutoResetEvent(false); RegisteredWaitHandle rwh = ThreadPool.RegisterWaitForSing leObject(are, new WaitOrTimerCallback(EventSignalled), null, -1, false); for (Int32 x = 0; x < 5; x++) { Thread.Sleep(5000); are.Set(); } rwh.Unregister(null); Console.WriteLine("Hit Enter to terminate the sample"); Console.ReadLine(); } static void EventSignalled(Object state, Boolean timedOut) { if (timedOut) { Console.WriteLine("Timed-out while waiting for the AutoResetEvent."); } else { Console.WriteLi e("The AutoResetEvent became signalled."); } } } }

程序结果如图1-10所示。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载