用多线程进行数据采集
一、 多线程数据采集的优点
Win95/98最让人喜爱的除了漂亮的界面以外,就是多线程与多任务了。在Windows 9 5以及Windows NT中,一个程序无法独占所有的CP U执行时间,一个程序也不是从头到尾一条线。相反,一个程序在执行中可以分为多个程序片段同时执行。这些能同时执行的程序片段称为线程。在Windows 95以及Windows NT中,操作系统同一时间可以轮流执行多个程序,这就是多任务。
采用多线程进行数据采集可以有效地加快程序的反应速度、增加执行的效率。一般的程序中都要处理用户的输入,但用户的输入速度与CPU的执行速度相比就向走路与坐飞机一样。这样,CPU就将浪费大量的时间用来等待用户的输入(如在DOS环境中)。如果采用多线程,那么就可以用一个线程等待用户的输入;另一个线程进行数据处理或其他的工作。对于数据采集程序,可以用一个单独的线程进行数据采集。这样,能最大限度地保证采集的实时性,而另外的线程同时又能及时地响应用户的操作或进行数据处理,否则,程序在采集数据时就不能响应用户的操作或在响应用户操作时就不能进行数据采集,尤其当采集的数据量很大、数据处理任务很重时,如果不采用多线程,采集时的漫长的等待是难以忍受的。
但是,多线程要比普通程序设计复杂得多。由于任一时刻都可能有多个线程同时执行,所以,许多的变量、数据都可能会被其他线程所修改。这就是多线程程序中最关键的线程间的同步控制问题。
二、 多线程数据采集应解决的问题
其实,多线程程序设计复杂是暂时的,如果你采用传统的C进行多线程的设计,那么你必须自己控制线程间的同步,那将是很复杂的。但是如果利用面向对象的设计方法,采用Delphi进行多线程程序设计,问题就简单多了。这是因为,Delphi已将多线程的复杂性替我们处理了, 我们所要做的就是继承。
具体地说,多线程数据采集需要完成以下工作:
1. 从TThread类派生一个自己的类Sample Thread,这就是我们用于数据采集的类,进行采集时,只需要简单地创建一个SampleThread。
2. 重载超类TThread的Execute方法,在这一方法中将具体地执行数据采集任务。
3. 如果希望一边采集一边显示,就再编写几个用于显示采集进度的过程,供Execute 方法调用。
TThread类中最常用的属性/方法如下:
Create方法:constructor Create(CreateSuspended: Boolean) 。
其中CreateSuspended参数确定线程在创建时是否立即执行。如果为True,新线程在创建后被挂起;如果为False,线程在创建后立即执行。
FreeOnTerminate属性:property FreeOnTerminate: Boolean。
该属性确定程序员是否负责撤消该线程。如果该属性为True,VCL 将在该线程终止时自动撤消线程对象。它的缺省值为False。
OnTerminate属性:property OnTerminate: TNotifyEvent。
该属性指定一个当线程终止时发生的事件。
下面看一个具体的例子。
三、多线程数据采集的实现
这是笔者开发的一个测抽油机功能的程序。它的功能是采集抽油机悬点的载荷及位移数据,经过处理后做出抽油机的功能图。图中所示是数据采集时的界面。点"采集数据"按钮后,程序将创建一新的线程,并设置其属性。这一新线程将完成数据采集任务。程序如下: 图1
Procedure TsampleForm.DoSampleBtnClick(Sender: TObject);
Begin
ReDrawBtn.Enabled := True;
DoSampleBtn.Enabled := False;
FFTBtn.Enabled := True;
TheSampler := SampleThread.Create(False);
←创建采集线程
TheSampler.OnTerminate := FFTBtnClick;
←采集完成后要执行的任务
TheSampler.FreeOnTerminate := True;
←采集完成后撤消
End;
采集线程的类定义如下:
Type
SampleThread = class(TThread)
Public
function AdRead(ach: byte): integer; safecall;
←读A/D卡的函数
procedure UpdateCaption; ←显示采集进度
procedure ShowCostTime; ←显示采集所用时间
private
{ Private declarations }
protected
thes, thep: real;
dt: real;
id: integer;
st, ed: LongInt;
procedure Execute; override;←这是关键。
End;
在这个类中定义了一个函数AdRead用于操作A/D卡,两个过程用于显示采集的进度与所用时间。需要注意的是AdRead函数是用汇编写的 ,参数调用格式必须是safecall。
关键的重载方法Execute的代码如下:
Procedure SampleThread.Execute;
Begin
StartTicker := GetTickCount;
id := 0;
Repeat
thes := Adread(15) * ad2mv * mv2l;→采集第15通道
thep := Adread(3) * ad2mv * mv2n;→采集第3通道
dt := GetTickCount - StartTicker;
sarray[id] := thes;
parray[id] := thep;
tarray[id] := dt;
inc(id);
Synchronize(UpdateCaption);→注意:显示采集进度 Until i d>=4096;
ed := GetTickCount; Synchronize(ShowCostTime);→注意 :显示所用时间
end;
从以上代码中可见,Execute与一般的代码并无本质区别。仅有的区别是显示采集进度和显示所用时间时,不能直接调用各自的过程,而是通过调用Synchronize间接地调用, 这样做是为了保持进程间的同步。
四、结论
以上的程序采用Delphi 4.0编程,在AMD-K6-2/300上实现。测试结果是这样的:采用多线程,采集4096个点一般耗用10~14秒的时间; 如果不采用多线程则需要1分钟到1分半钟。可见多线程可明显提高程序的执行效率。
《计算机世界报》99年第二十期(5月31日): 电脑与生活
河北固安华北石油职工大学 袁一林 李晓平