前言
TMonitor 是用于同步线程的一个记录。说清楚点,我们讨论的是Delphi 的 System.TMonitor 单元 , 而不是 Vcl.Forms.TMonitor 单元。
自Delphi 2009年以来,TObject实例的大小增加了一倍,以便增加额外的4个字节。这4个字节是干什么的?事实上就是提供TMonitor支持!现在每个继承自TObject的对象都可以使用TMonitor锁。TMonitor记录,它实现了一个通用的监视器同步的结构。
准备
在这个文章中,我们将讨论一个经典的多线程访问共享文件的并发问题。具体的就是,我们有很多线程同时访问一个共享文件,所有的线程必须进行同步。如果不进行同步,将会导致程序出现共享错误,解决这个线程同步问题也有很多方法,无疑TMonitor是最简单的方法。开始吧...
如何做?
按照如下步骤使用TMonitor同步共享资源。
- 创建一个新的 VCL 程序(菜单 File | New | VCL Forms Application);
- 在主Form上放置一个TButton, TListBox, 和 TTimer 控件;
- 命名TButton为 btnStart,标题更改为:写入共享文件;
- 在项目中增加一个单元,命名为: FileWriterThreadU.pas,同时写入如下代码:
unit FileWriterThreadU;
interface
uses
System.Classes, System.SyncObjs, System.SysUtils, System.IOUtils;
type
TThreadHelper = class helper for TThread
public
function WaitFor(ATimeout: Cardinal): LongWord; platform;
end;
TFileWriterThread = class(TThread)
private
FStreamWriter: TStreamWriter;
protected
procedure Execute; override;
public
constructor Create(AStreamWriter: TStreamWriter);
end;
implementation
{$IF Defined(MSWINDOWS)}
uses
Winapi.Windows;
{$IFEND}
constructor TFileWriterThread.Create(AStreamWriter: TStreamWriter);
begin
FStreamWriter := AStreamWriter;
inherited Create(False);
end;
procedure TFileWriterThread.Execute;
var
I: Integer;
NumLines: Integer;
begin
inherited;
NumLines := 11 + Random(50);
for I := 1 to NumLines do
begin
TThread.Sleep(200);
//here we are locking the shared resource
TMonitor.Enter(FStreamWriter);
try
FStreamWriter.WriteLine(Format('THREAD %5d - ROW %2d',
[TThread.CurrentThread.ThreadID, I]));
finally
//unlock the shared resource
TMonitor.Exit(FStreamWriter);
end;
if Terminated then
Break;
end;
end;
function TThreadHelper.WaitFor(ATimeout: Cardinal): LongWord;
begin
{$IF Defined(MSWINDOWS)}
Result := WaitForSingleObject(Handle, ATimeout);
{$ELSE}
raise Exception.Create('Available only on MS Windows');
{$IFEND}
end;
initialization
Randomize; // we'll use Random function in the thread
end.
5. 返回主From单元,在接口部分增加如下引用:
- System.Generics.Collections
- FileWriterThreadU
6. 在 From 的private节增加如下变量:
private
FOutputFile: TStreamWriter;
FRunningThreads: TObjectList<TFileWriterThread>
7. 在 From的 FormCreate 和 FormClose事件中增加如下代码:
procedure TMainForm.FormCreate(Sender: TObject);
begin
FRunningThreads := TObjectList<TFileWriterThread>.Create;
FOutputFile := TStreamWriter.Create(TFileStream.Create('OutputFile.txt',
fmCreate or fmShareDenyWrite));
end;
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
var
Th: TFileWriterThread;
begin
for Th in FRunningThreads do
Th.Terminate;
FRunningThreads.Free; // Implicit WaitFor...
FOutputFile.Free;
end;
前面的代码,创建了一个数据结构来保存线程列表和文件访问。FOutputFile变量是所有线程访问的共享资源。
8. 给 btnStart 按键的 Click 事件增加如下代码:
procedure TMainForm.btnStartClick(Sender: TObject);
var
I: Integer;
Th: TFileWriterThread;
begin
for I := 1 to 10 do
begin
Th := TFileWriterThread.Create(FOutputFile);
FRunningThreads.Add(Th);
end;
end;
以上代码创建10个线程,共享文件 FOutputFile
9. 现在程序已经可以没有问题的跑起来了,但是在UI层面,我们并不清楚每个线程的情况,所以增加一个定时器,用来显示线程的情况。定时器检查线程列表,并将线程的状态显示在 ListBox1 中。
procedure TMainForm.Timer1Timer(Sender: TObject);
var
Th: TFileWriterThread;
begin
ListBox1.Items.BeginUpdate;
try
ListBox1.Items.Clear;
for Th in FRunningThreads do
begin
if Th.WaitFor(0) = WAIT_TIMEOUT then
ListBox1.Items.Add(Format('%5d RUNNING', [Th.ThreadID]))
else
ListBox1.Items.Add(Format('%5d TERMINATED', [Th.ThreadID]))
end;
finally
ListBox1.Items.EndUpdate;
end;
end;
10. 运行程序,按下 btnStart 按键,结果界面:
11. 程序运行结束后,我们可以检查共享文件,没有任何错误发生,数据也没有丢失,线程共享运行良好。