[教程] ZValueWatch 教程之一:在主线程中监测值变化

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);

【下载】

下载二进制示例程序

分享到: