[QWorker] 制作多线程日志输出查看Demo

要解决的问题: 有多个线程输出日志,日志内容需要在列表框中显示出来,不管日志输出的频率快慢,界面不能卡,不能闪烁。超过10万行日志时,自动删除最开始的1万行日志。

此问题涉及多线程编程,多线程输出时要更新界面的显示。

多线程的东西,当然不能忘了QWorker这样的神器,下面我们就来使用QWorker解决问题,哦,不对,是用YxdWorker来解决问题(为什么是YxdWorker?哈,这是宝宝的修改版本啦,封装成了自己喜欢的样子,其实和QWorker差不多,有兴趣可以到 SVN:  https://github.com/yangyxd/YxdWorker下载。)

一、先在在搜索路径中添加对YxdWorker的引用,然后在窗体上放一个ListView, Name=ListView1,再放一个CheckBox, Name=CheckBox1。效果如下:

QQ截图20160122210453

ListView1的OwnerData属性设为True,ViewStyle属性设为vsReport,添加一列“日志内容”。

 

二、 开始写代码

unit Unit1;

interface

uses
  YxdWorker, SyncObjs, CommCtrl,
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ComCtrls, StdCtrls;

type
  TForm1 = class(TForm)
    CheckBox1: TCheckBox;
    ListView1: TListView;
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Button4: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure ListView1Data(Sender: TObject; Item: TListItem);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure CheckBox1Click(Sender: TObject);
  private
    { Private declarations }
    FLogs: TStrings;       // 存放日志内容
    FAutoScroll: Boolean;  // 是否自动滚动
    FLogsIsDel: Boolean;   // 是否已经删除日志
    FLogRef: Integer;      // 状态计数器
    FLocker: TCriticalSection;

    FTestRef: Integer;
  public
    { Public declarations }
    procedure DoDataChange();
    procedure DoWriteLog(Sender: TObject; const Log: string);
    procedure OnDataChange(AJob: PJob);

    procedure DoTest(AJob: PJob);
    procedure Log(const Text: string);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  Workers.Post(DoTest, nil);
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  FLocker.Enter;
  FLogs.Clear;
  FLogsIsDel := True;
  FLocker.Leave;
  DoDataChange();
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
  Log('写一行日志');
end;

procedure TForm1.Button4Click(Sender: TObject);
begin
  Workers.Clear(DoTest, nil);
end;

procedure TForm1.CheckBox1Click(Sender: TObject);
begin
  FLocker.Enter;
  FAutoScroll := CheckBox1.Checked;
  FLocker.Leave;
end;

procedure TForm1.DoDataChange;
begin
  // FLogRef 很关键,决定什么时候真正更新显示。
  // 多线程写日志时,FLogRef > 1,那么也只更新一次。
  if InterlockedIncrement(FLogRef) = 1 then
    // 延时50ms更新。此值设定的越大,列表更新的越慢。
    // 不延时的话,界面刷新太快,占用资源会比较大
    Workers.Post(OnDataChange, nil, True, 50)
  else
    InterlockedDecrement(FLogRef);
end;

procedure TForm1.DoTest(AJob: PJob);
var
  I, M: Integer;
begin
  M := 0;
  while (not AJob.IsTerminated) and (M < 10000) do begin
    Inc(M);
    I := InterlockedIncrement(FTestRef);
    Log(Format('日志内容。工作者:%d. (%d)', [AJob.Handle, I]));
    Sleep(100);
  end;
end;

procedure TForm1.DoWriteLog(Sender: TObject; const Log: string);
var
  I: Integer;
begin
  if Assigned(FLogs) and (Assigned(Self)) then begin
    FLocker.Enter;
    // 大于10万行时,删除前面的1万行
    if FLogs.Count > 100000 then begin
      FLogsIsDel := True;
      for I := 10000 downto 0 do
        FLogs.Delete(I);  
    end;
    // 添加当前日志内容
    FLogs.Add('[' + FormatDateTime('hh:mm:ss.zzz', Now) + '] ' + Log);
    FLocker.Leave;
    // 产生一个变更通知
    DoDataChange();
  end;
end;

// 初始化
procedure TForm1.FormCreate(Sender: TObject);
begin
  FLogsIsDel := False;
  FLogRef := 0;
  FTestRef := 0;
  FLogs := TStringList.Create();
  FLocker := TCriticalSection.Create;
  FAutoScroll := CheckBox1.Checked;
  // 将最大工作者数量设置大一些。因为太少的话,工作者不够用,没有时间来显示日志了。
  Workers.MaxWorkers := 512;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  Workers.Clear(Self);
  FreeAndNil(FLogs);
  FreeAndNil(FLocker);
end;

procedure TForm1.ListView1Data(Sender: TObject; Item: TListItem);
begin
  FLocker.Enter;
  if Assigned(FLogs) and (Item.Index < FLogs.Count) then
    Item.Caption := FLogs[Item.Index];
  FLocker.Leave;
end;

procedure TForm1.Log(const Text: string);
begin
  if Assigned(Self) then    
    DoWriteLog(Self, Text);
end;

procedure TForm1.OnDataChange(AJob: PJob);
begin
  if Assigned(Self) and Assigned(FLogs) then begin
    if Assigned(ListView1) and (ListView1.HandleAllocated = True) then begin
      ListView_SetItemCountEx(ListView1.Handle, FLogs.Count,
        LVSICF_NOINVALIDATEALL or LVSICF_NOSCROLL); // 修改列表项的数量,并不改变滚动条位置
      if FAutoScroll then
        SendMessage(ListView1.Handle, WM_VSCROLL, SB_BOTTOM, 0);
      if FLogsIsDel then begin
        FLogsIsDel := False;
        ListView1.Invalidate;
      end;
    end;
    InterlockedDecrement(FLogRef);
  end;
end;

end.

核心有两点:

  1.  使用FLogRef计算器作为实际更新界面的依据。
  2. 使用ListView_SetItemCountEx宏来代替ListView.Count=xxxx更新列表框的行数。
  3. 使用临界锁来保持线程同步。

源码下载:

LogView_Demo

 

分享到: