Z 系列又加入了新的成员:ZValueWatch,它被实现用来异步监测一个值的变化。
IZValueWatch 通过将赋值和变更通知进行了异步化,这样子,在值频繁变化的场景,可以有效的减少变更通知的次数,从而提升程序的运行效率。
我们假设一个典型的应用场景:我们在后台线程中要执行长时间的操作,并且及时更新前台的进度显示。在一般的情况下,我们为了显示进度,使用 TThread.Queue/Synchronize 来让程序更新主界面的进度显示,如果我们频繁更新进度信息,就会频繁的在后台线程和主线程中进行切换,影响后台线程的执行效率。而实际上,进度的中间显示内容是可忽略的内容,我们只需要保证其值在界面渲染时,能够读取到最新的就可以了。
ZValueWatch 实现了这一机制,我们可以在后台操作时,随时更新进度,而这个进度的更新并不会立即反馈到主界面,而是会在主线程中检查这一变动,然后将其呈现给用户。
我们现在按顺序来实现这一个功能:
1、定义一个进度提示的记录类型,用于缓存进度信息:
type
TProgressHint = record
Progress: Integer;
Hint: String;
end;
2、我们在窗体添加需要的控件,然后定义的私有部分,声明一个进度变更监视变量 FProgressWatch,为了便于观察,我们额外增加一个 FInvokeTimes 来记录监视变量变更事件触发的次数:
TForm1 = class(TForm)
...
private
{ Private declarations }
FInvokeTimes: Integer;
FProgressWatch: IZValueWatch<TProgressHint>;
public
{ Public declarations }
end;
3、双击窗体,在其 OnCreate 事件响应中,定义这个变量及其值变更时的响应:
procedure TForm1.FormCreate(Sender: TObject);
begin
FProgressWatch := TZWatchItems.Current.CreateWatch<TProgressHint>(
procedure(const AValue: TProgressHint)
begin
Inc(FInvokeTimes);
ProgressBar1.Position := AValue.Progress;
Label1.Caption := AValue.Hint;
Label1.Update;
if AValue.Progress = 100 then
begin
Button1.Caption := 'Start Progress';
Button1.Enabled := true;
Memo1.Lines.Add(FormatDateTime('hh:nn:ss.zzz', Now) +
' Watch change monitor times:' + FInvokeTimes.ToString);
end;
end);
end;
4、双击窗体的 Start Progress 按钮,在其 OnClick 事件中创建一个匿名线程,来循环1000万次
procedure TForm1.Button1Click(Sender: TObject);
begin
Button1.Caption := 'Processing...';
Button1.Enabled := false;
FInvokeTimes := 0;
Memo1.Lines.Add(FormatDateTime('hh:nn:ss.zzz', Now) + ' Start process ...');
TThread.CreateAnonymousThread(
procedure
var
AProgress: TProgressHint;
ACount: Integer;
ATime: Cardinal;
const
PassCount = 10000000;
begin
ACount := 0;
ATime := TThread.GetTickCount;
while (not Application.Terminated) and (ACount < PassCount) do
begin
AProgress.Progress := ACount * 100 div PassCount;
AProgress.Hint := ACount.ToString;
Inc(ACount);
FProgressWatch.Value := AProgress;
end;
AProgress.Hint := (TThread.GetTickCount - ATime).ToString + 'ms';
AProgress.Progress := 100;
FProgressWatch.Value := AProgress;
end).Start;
end;
注意我们上面的写法,线程中只修改局部变量的值,在更新完成后,一次性赋值给 FProgressWatch.Value。这里是因为我们的 Value 是一个整体,是通过 GetValue 函数返回的临时变量,写成 FProgressWatch.Value.Progress := 值 的形式对 FProgressWatch.Value 的值,不会产生任何影响。
5、现在我们运行程序,并点击 Start Progress 按钮,就会看到进度条和提示文本在跟随后台线程处理的进度快速变化。
也就是说,我们这个实现要分三步走:定义观察的值类型,创建观察接口实例(TZWatchItems.Current.CreateWatch)、修改观察接口实例的 Value 值。
【注意】
1、要检查值的变动,我们需要调用 TZWatchItems.CheckChanges 函数,默认实现提供了三种方式:
- 手动检查(wkNone):由程序员自己去负责触发,如果您
- 定时检查(wkTimer):当有需要监视的对象时,每16ms(相当于 60 FPS)会自动检查一次(默认)
- 消息驱动(wkMessage):当监视的对象值发生变动时,会向主线程投递 WM_NULL 消息,应用会在 WM_NULL 消息的处理里,触发 TZWatchItems.CheckChanges 的调用。
2、TZWatchItems.CheckChanges 必需在主线程中执行,后台线程执行会直接报错。
3、如果要在线程中检查某个值的变动,那么可以调用 CreateWatch 返回的接口实例的 CheckChanged 函数,如果发生变动,会触发通知回调函数。CheckChanged 本身是线程安全的,但是在这种使用场景下,用户应该保证通知回调函数里的操作是线程安全的。一般我们并不推荐这样做,这样做实际上违背了我们的设计初衷。
4、推荐用户根据实际需要,调整定时检查的间隔。
5、极限情况下,如果多个线程同时更新一个值,则只有第一次更新的值会被记录,其它值会被忽略。这种情况下,无论是用户还是程序本身,实际上都不能准确区分应该保留那个值,程序这里会检查 FUpdateRefCount 是否为 1,不为 1 的值会被忽略掉。
【更多示例】
假设我们要更新某个控件的位置和大小,按上面的的步骤,我们只需要下面的代码就可以了:
//定义变量
private
FControlBoundsWatch:IZValueWatch<TRect>;
....
//初始化
FControlBoundsWatch:=TZWatchItems.Current.CreateWatch<TRect>(
procedure (const R:TRect)
begin
Label1.SetBounds(R.Left,R.Top,R.Width,R.Height);
end);
....
//赋值
FControBoundsWatch.Value:=Rect(100,20,300,40);
【下载】