(************************************************
(* Subject: 线程杂谈3
(* Author: linzhenqun(风)
(* Time: <chsdate w:st="on" year="2006" month="3" day="25" islunardate="False" isrocdate="False">2006-3-25</chsdate>
(* Blog: http://blog.csdn.net/linzhengqun
(* E-mail: linzhengqun@163.com
(************************************************
前言
我在写完线程杂谈2之后,本来不再打算写关于线程的文章了,但由于项目中时时要与线程打交道,所以于实践中又领悟了一些技巧,于是又有了此篇。
学习Windows的消息循环
我在做Call Center项目时,负责一个邮件服务器程序,座席端软件可以通过该邮件服务器收取邮件,也可以通过它发送邮件。发送邮件的时候我开始的设计是这样的,一个座席发送一封邮件过来,邮件服务器收到这封邮件后即启动一个线程负责将它发送出去,但这样做是有严重的性能问题的,假设如果有十个座席同时发送邮件,则邮件服务器必须启动十个线程,如果十个座席每个人同时发送几封邮件,邮件服务器即必须启动几十个线程,显然这样做是不符合实际的。
有没有办法解决这个瓶颈呢,正当我苦苦思索的时候,想到了Windows的消息队列和消息循环,对于每一个应用程序,Windows都为它维护一个消息队列,当由于键盘鼠标等硬件事件发生时,windows将相应的消息结构加入到应用程序的消息队列中。如果我们写过Windows的程序,就知道它的入口函数中必须有一个循环,不断地从消息队列中取出消息,然后发送至处理该消息函数中。
这样的技术很好的解决了并发性带来的问题,使得每个动作都必须排队,那么发送邮件其实也可以用这样的技术来解决:程序运行的过程中,有一个负责发送邮件的工作线程,它一直循环从发送队列中取出发送邮件的简要信息,程序根据这个信息从数据库中取出邮件发送出去。不过这里得注意线程同步的问题,有可能在将发送信息加入队列的同时,线程正在取队列,所以要用一个临界区保证不会发生竞争条件。
下面这个方法的示例代码:
unitUnit1;
interface
uses
Windows,Messages,SysUtils,Variants,Classes,Graphics,Controls,Forms,
Dialogs,StdCtrls;
type
//模拟邮件在数据库的信息
PSendRec=^TSendRec;
TSendRec=record
SendID:string;
end;
TSendEvent=procedure(SendRec:PSendRec)ofobject;
TSendTread=class(TThread)
private
FLock:TRTLCriticalSection;//声明一个临界区变量
FSendQue:TList;//发送结构的队列
FSendRec:PSendRec;
FSendEvent:TSendEvent;
procedureLock;
procedureUnLock;
procedureClearQueue;//清除队列
procedureSendAction(SendRec:PSendRec);//模拟发送的动作
protected
procedureExecute;override;
procedureDoSend;
public
constructorCreate(Suspend:Boolean);
destructorDestroy;override;
//将一个发送结构加入队列
procedureAddToQueue(SendRec:PSendRec);
//从队列中取出一个发送结构
functionPopFromQueue:PSendRec;
propertySendEvent:TSendEventreadFSendEventwriteFSendEvent;
end;
TForm1=class(TForm)
btnSend:TButton;
edtSendID:TEdit;
edtSendResult:TEdit;
procedureFormCreate(Sender:TObject);
procedureFormDestroy(Sender:TObject);
procedurebtnSendClick(Sender:TObject);
private
{Privatedeclarations}
SendThread:TSendTread;
procedureOnSend(SendRec:PSendRec);
public
{Publicdeclarations}
end;
var
Form1:TForm1;
implementation
{$R*.dfm}
{TSendTread}
procedureTSendTread.AddToQueue(SendRec:PSendRec);
begin
Lock;
try
FSendQue.Add(SendRec);
finally
UnLock;
end;
end;
procedureTSendTread.ClearQueue;
var
i:Integer;
begin
fori:=0toFSendQue.Count-1do
Dispose(FSendQue[i]);
FSendQue.Clear;
end;
constructorTSendTread.Create(Suspend:Boolean);
begin
inheritedCreate(Suspend);
InitializeCriticalSection(FLock);
FSendQue:=TList.Create;
end;
destructorTSendTread.Destroy;
begin
//下面的技术在以前的文章已经说过了
Terminate;
WaitFor;
ClearQueue;
FSendQue.Free;
DeleteCriticalSection(FLock);
inherited;
end;
procedureTSendTread.DoSend;
begin
ifAssigned(FSendEvent)then
FSendEvent(FSendRec);
end;
procedureTSendTread.Execute;
var
SendRec:PSendRec;
begin
whilenotTerminateddo
begin
SendRec:=PopFromQueue;
ifSendRec<>nilthen
SendAction(SendRec);
Sleep(50);//稍作休息,避免占用CPU过多
end;
end;
procedureTSendTread.Lock;
begin
EnterCriticalSection(FLock);
end;
functionTSendTread.PopFromQueue:PSendRec;
begin
Result:=nil;
Lock;
try
ifFSendQue.Count>0then
begin
Result:=FSendQue[0];
FSendQue.Delete(0);
end;
finally
UnLock;
end;
end;
procedureTSendTread.SendAction(SendRec:PSendRec);
begin
FSendRec:=SendRec;
Synchronize(DoSend);
Dispose(SendRec);
Sleep(500);
end;
procedureTSendTread.UnLock;
begin
LeaveCriticalSection(FLock);
end;
procedureTForm1.FormCreate(Sender:TObject);
begin
edtSendID.Text:='0';
SendThread:=TSendTread.Create(True);
SendThread.SendEvent:=OnSend;
SendThread.Resume;
end;
procedureTForm1.FormDestroy(Sender:TObject);
begin
SendThread.Free;
end;
procedureTForm1.OnSend(SendRec:PSendRec);
begin
//接收事件,显示已经处理完的ID
edtSendResult.Text:=SendRec^.SendID;
end;
procedureTForm1.btnSendClick(Sender:TObject);
var
SendRec:PSendRec;
i:Integer;
begin
New(SendRec);
SendRec^.SendID:=edtSendID.Text;
//重生成一个ID,递增
i:=StrToInt(edtSendID.Text);
Inc(i);
edtSendID.Text:=IntToStr(i);
SendThread.AddToQueue(SendRec);
end;
end.
代码中用SendRec模拟发送的结构,里面只是一个简单的SendID,线程类中有AddToQueue和PopFromQueue两个方法,分别是将一个结构加进队列尾和从队列头取出一个结构,这两个方法用Lock和UnLock将操作锁起来,成为一个原子操作防止竞争条件的出现。而Execute的操作就是不断的循环从队列取结构,如果队列不为空,将取出的结构传递给SendAction方法,我们可以假定这个方法就是发送邮件的方法,为了显示效果,我特别在该方法中向外发布一个事件,以该结构为参数,回调完事件后,即可将该结构的内存清除。
再看主窗体,程序一开始就创建了线程类,用按钮模拟发送邮件的操作,快速连续的按BtnSend,将产生一个个发送结构,并赋给一个唯一的ID,然后进加线程的发送队列中。这时候线程检测到队列中有数据,马上处理并向主界面发送事件,主界面在事件中显示了该结构的ID。
这个过程很有趣,无论我们怎样疯狂的点击按钮,edtSendResult总是有条不紊地显示结构ID。
用并发的队列提高效率
有经验的程序也许会看出来,用上面的方法虽然可以保证程序的性能,但效率可是低了很多,如果同时有一百封邮件在队列中,假如每发送一封邮件平均用时1秒,则第一百封邮件要过一分钟钟才能被发送,这显然实时性不够。程序有时就是这样,需要在各个方面作一个权衡,好象能量守恒定律一样,如果动能增加了则势能就减少了。我们可以平衡这种极端性,在保证程序的稳定性能同时,也要提高程序的效率。
有什么办法呢,还是要用多线程来做,假设有一个线程池类,每一个线程维护一个有限的队列,如果一个线程的队列达到最大值时,就会将结构加到另一个线程的队列中,线程池类管理线程,如果线程数不足,它会自动生成新的线程提供使用,这类似于内存页的管理技术。
在主程序中我们只和线程池类打交道,假设这个线程池为TsendTrdPool,将每一个SendID传送进TsendTrdPool的一个方法,同时要传进一个回调函数,TsendTrdPool会将其挂到线程类中,这样界面便可以显示Send的结果了。
声明一个MaxQueLen常量,定义发送队列最大的长度,对于线程类来说,只需要在AddToQueue中加一个限制,如果队列已经达到MaxQueLen,则增加失败,上面的线程类实现代码不必作过多的修改,只需将AddToQueue改成下面的样子:
functionTSendTread.AddToQueue(SendRec:PSendRec):Boolean;
begin
Lock;
try
Result:=FSendQue.Count<MaxQueLen;
ifResultthen
FSendQue.Add(SendRec);
finally
UnLock;
end;
end;
我们假设MaxQueLen为10,接下来重点实现TsendTrdPool,且看下面的代码:
type
...
TSendTrdPool=class
private
FSendTrdList:TList;
//清除发送线程
procedureClearSendThreadList;
//创建一个新的线程类
functionCreateNewThread:TSendTread;
functionGetCount:Integer;
public
//加进发送记录,并传进一个回调函数
procedureAddSendRec(SendRec:PSendRec;ASendEvent:TSendEvent);
constructorCreate;
destructorDestroy;override;
propertyCount:IntegerreadGetCount;
end;
implementation
...
{TSendTrdPool}
procedureTSendTrdPool.AddSendRec(SendRec:PSendRec;
ASendEvent:TSendEvent);
var
Succ:Boolean;
i:Integer;
SendThread:TSendTread;
begin
Succ:=False;
fori:=0toFSendTrdList.Count-1do
begin
SendThread:=TSendTread(FSendTrdList[i]);
SendThread.Lock;
try
ifSendThread.AddToQueue(SendRec)then
begin
Succ:=True;
SendThread.SendEvent:=ASendEvent;
Break;
end;
finally
SendThread.UnLock;
end;
end;
ifnotSuccthen
begin
SendThread:=CreateNewThread;
SendThread.SendEvent:=ASendEvent;
SendThread.AddToQueue(SendRec);
end;
end;
procedureTSendTrdPool.ClearSendThreadList;
var
i:Integer;
begin
fori:=0toFSendTrdList.Count-1do
TSendTread(FSendTrdList[i]).Free;
end;
constructorTSendTrdPool.Create;
begin
FSendTrdList:=TList.Create;
end;
functionTSendTrdPool.CreateNewThread:TSendTread;
begin
Result:=TSendTread.Create(False);
FSendTrdList.Add(Result);
end;
destructorTSendTrdPool.Destroy;
begin
ClearSendThreadList;
FSendTrdList.Free;
inherited;
end;
functionTSendTrdPool.GetCount:Integer;
begin
Result:=FSendTrdList.Count;
end;
end.
这个类保存一个发送线程列表,初始化时这个列表为0,当AddSendRec被调用时,它会把一个发送结构和事件尝试加进列表中的某个线程,如果加入失败,表明所有线程的发送队列均已达到最大值,此时线程池类自动增加一个新的线程,并将发送结构加进这个类中。具体可看上面的实现代码,其中有一点要注意,如果增加了的线程将不会被消毁,只有到线程池类被消毁时,所有线程才被消毁。
现在我们来看看主界面的反应,在主窗体创建时生成一个TsendTrdPool,程序结束时消毁它,界面有一个按钮,其事件代码如下:
procedureTForm1.btnSendClick(Sender:TObject);
var
SendRec:PSendRec;
i:Integer;
begin
fori:=0to99do
begin
New(SendRec);
SendRec^.SendID:=IntToStr(i);
SendTrdPool.AddSendRec(SendRec,OnSend);
end;
//显示线程池中的线程数
edtThreadCount.Text:=IntToStr(SendTrdPool.Count);
end;
点击一下按钮,将生成100个发送结构,在Edit中会显示一共有多少个线程,点击一下,一般生成10个线程,快速地点击两下,一般生成18或19个线程,这是很容易理解的,因为在点击的过程中,某个线程的发送队列已有空余,所以不需要产生新的线程,所以点击的次数越多,线程增长会越慢。
所以这个技术是很有用的,可以有效地提高发送的效率,同时也减少线程的数量,达到某种奇妙的平衡效果。
也许有会问,如果邮件同时发送非常非常多,线程一样会有非常多的数量,我想这个情况应该是极少会出现,且邮件同时发送越多,线程增长的速度是越慢的,因为线程也一边在发送邮件,队列也一边在减少啊。如果真的要追究这个问题,还是有办法可以解决的,即线程池中的线程数量也是有限的,而同时线程池类中有另一个线程假设为AddThread,它的职责就是将发送结构加进某个发送线程中,当发送线程数没有达到极限时,它当然可以很快地把发送结构加进去,如果线程数达到极限,则它会一直循环去判断哪个发送线程的队列有空余,发现之后再加进去。这样可算将线程发挥到另一个层次了,不过我想程序要想复杂到这个程度还是比较少见的,兴趣的你可以自己去实现吧。
后记
关于线程的应用实在是太多了,我想,线程杂谈也许还会有第四篇,第五篇,不过这些方法大多来源于实践,都是为了解决某些问题而进行思考,借鉴以及尝试的结果。有很多经典的技术其实很值得借鉴,我们有时候看看VCL,想想Win32,都能从中得到很多启发,所以,多体会一些成熟的思想,多阅读一些成功的代码,将会使你的技术大大提高。
分享到:
相关推荐
### 多核多线程杂谈-并行计算 #### 1. 并行计算概述 随着计算机硬件的发展,单核处理器的性能提升遇到了物理瓶颈,因此多核处理器成为了提高计算能力的关键技术之一。并行计算是利用多核处理器或多台计算机协同...
#### 3. 多核多线程编程面临的挑战 - **资源共享与同步**:在多线程环境中,线程之间需要共享内存和其他资源。当多个线程尝试访问同一资源时,可能会出现竞争条件,导致数据不一致或其他错误。解决这类问题通常需要...
《多线程编程指南》是由SUN公司出版的一本深入探讨多线程编程的重要书籍,对于想要提升在并发处理方面技能的程序员来说,这是一份不可多得的学习资源。本书全面讲解了Java语言中的线程相关知识,涵盖了从基本概念到...
3. **模型-视图-控制器(MVC)模式**: Swing设计时遵循MVC模式,组件如JTable和JList采用此模式,将数据(模型)与显示(视图)分离,允许独立更新和定制。例如,通过DefaultListModel管理JList的数据模型。 4. **...
标题和描述中的"杂谈,一些工具类的集合"可能指的是一个涵盖多种工具的资源包,旨在解决日常开发中的各种问题。 首先,我们来探讨一下工具类在编程中的作用。在编程中,工具类通常是一组静态方法的集合,这些方法...
第二十六,当一个线程进入对象的synchronized方法,其他线程不能进入该对象的其他synchronized方法,除非当前线程退出了同步方法。 第二十七,try后的finally块总会被执行,无论是否有return语句。return前执行...
3. **使用TSRM**:在PHP内核中,从全局传递参数时几乎都需要使用TSRM,以确保线程之间的正确隔离。 #### 五、PHP流 除了直接使用套接字进行网络操作外,PHP还提供了一个更高级别的抽象层——PHP流。PHP流是一种...
"高并发场景杂谈.zip"这个压缩包文件集成了多种处理高并发问题的策略和技术,旨在为开发者提供解决高并发问题的思路和实践案例。下面将详细讨论其中涉及的知识点。 首先,我们来看"Redis专场:如何利用Redisson...
本文将基于“Android开发杂谈”的主题,结合提供的资源——一个名为"Android_.pdf"的文件,来深入探讨一些重要的知识点。 1. **源码阅读**: 在Android开发中,理解源码是提升技能的关键。Android开源项目(AOSP)...
linux
(五)——传了值还是传了引用(六)——字符串(String)杂谈 (七)——日期和时间的处理 (八)——聊聊基本类型(内置类型)(九)——继承、多态、重载和重写(十)——话说多线程 (十一)——这些运算符你是否...
3、变量(属性)的覆盖;4、final,finally,finalize;5.传了值还是传了引用;6.String杂谈;7.日期与时间的处理;8.基本类型总结;9.继承,多态,重载,重写;10.多线程;11.运算符总结。 适合将要笔试面试Java的...
`client`版本通常适用于内存有限的桌面环境,而`server`版本则针对服务器环境,优化了多线程和大内存应用的性能。 理解这些基础知识对于Java程序员来说至关重要,它们不仅能够帮助我们编写更高效的代码,还能让我们...
3. 变量(属性)的覆盖:在JAVA中,子类可以覆盖父类的方法和属性。如果子类定义了一个与父类同名的属性,那么子类的属性将覆盖父类的属性。这种覆盖行为仅限于属性,不适用于方法重载(即方法名相同,参数列表不同...
3. **虚拟机原理**:JVM的运行主要包括类加载、字节码解析、内存管理、垃圾回收等过程。类加载涉及类的加载、验证、准备、解析和初始化五个阶段。字节码解析是将字节码转化为机器可执行的指令。内存管理则涵盖对象的...
3. 实时调度算法设计: - 设计目的是理解处理机调度算法,掌握EDF(最早截止期优先)和RMS(速率单调调度)算法的可调度条件,并能在可调度情况下展示具体调度结果。 - 在Linux环境下,通过用户级线程模拟实现EDF...
3. **变量(属性)的覆盖**: 在Java中,子类可以覆盖父类的方法,但不能覆盖变量。如果子类定义了一个与父类同名的变量,那么子类的变量会隐藏掉父类的变量,而不是覆盖它。访问控制的不同会影响这种情况下的可见...