从主线程更改线程的变量值是否安全?

It is safe to change variable values of a thread from the main thread?

我写了一个简单的组件来监视文件夹并在检测到更改时触发事件。它运作良好......显然。但我不确定一件事。有时,主线程可能需要更新被监控的路径,我不确定我是否做对了。它是关于 SetNewPath 程序的。这是从主线程执行的,它从另一个线程更改 UpdatePath 变量。当主线程写入 UpdatePath 而组件线程试图在 Execute 循环中读取其值时,可能会产生冲突 ?

FolderMonitor.pas

unit FolderMonitor;

interface

uses
  SysUtils, Windows, Classes, ExtCtrls;

type
  TOnFolderChange = procedure(Sender: TObject) of object;

  TFolderMonitor = class(TThread)
  private
    MainWait: THandle;
    UpdatePath: Boolean;
    TimeOut: Cardinal;
    FPath: String;
    FOnFolderChange: TOnFolderChange;
    procedure DoOnFolderChange;
    procedure SetNewPath(Path:String);
  protected
    procedure Execute; override;
  public
    constructor Create(const FolderPath: String; OnFolderChangeHandler: TOnFolderChange);
    destructor  Destroy; override;
    procedure   Unblock;
    property    Path: String read FPath write SetNewPath;
    property    OnFolderChange: TOnFolderChange read FOnFolderChange write FOnFolderChange;
  end;

implementation

constructor TFolderMonitor.Create(const FolderPath: String; OnFolderChangeHandler: TOnFolderChange);
begin
  inherited Create(True);
  FOnFolderChange:=OnFolderChangeHandler;
  FPath:=FolderPath;
  UpdatePath:=false;
  FreeOnTerminate:=false;
  MainWait:=CreateEvent(nil,true,false,nil);
  Resume;
end;

destructor TFolderMonitor.Destroy;
begin
  CloseHandle(MainWait);
  inherited;
end;

procedure TFolderMonitor.DoOnFolderChange;
begin
  if Assigned(FOnFolderChange) then
  Synchronize(procedure
  begin
   FOnFolderChange(Self);
  end);
end;

procedure TFolderMonitor.Unblock;
begin
  PulseEvent(MainWait);
end;

procedure TFolderMonitor.SetNewPath(Path:String);
begin
  FPath:=Path;
  UpdatePath:=true;
  PulseEvent(MainWait);
end;

procedure TFolderMonitor.Execute;
var Filter,WaitResult: Cardinal;
    WaitHandles: array[0..1] of THandle;
begin
  Filter:=FILE_NOTIFY_CHANGE_DIR_NAME + FILE_NOTIFY_CHANGE_FILE_NAME + FILE_NOTIFY_CHANGE_SIZE;
  WaitHandles[0]:=MainWait;
  WaitHandles[1]:=FindFirstChangeNotification(PWideChar(FPath),false,Filter);
  TimeOut:=INFINITE;

  while not Terminated do begin
   if UpdatePath then begin
    if WaitHandles[1]<>INVALID_HANDLE_VALUE then FindCloseChangeNotification(WaitHandles[1]);
    WaitHandles[1]:=FindFirstChangeNotification(PWideChar(FPath),false,Filter);
    TimeOut:=INFINITE;
    UpdatePath:=false;
   end;

   if WaitHandles[1] = INVALID_HANDLE_VALUE
    then WaitResult:=WaitForSingleObject(WaitHandles[0],INFINITE)
    else WaitResult:=WaitForMultipleObjects(2,@WaitHandles,false,TimeOut);

   case WaitResult of
    WAIT_OBJECT_0: Continue;
    WAIT_OBJECT_0+1: TimeOut:=200;
    WAIT_TIMEOUT: begin DoOnFolderChange; TimeOut:=INFINITE; end;
   end;

   if WaitHandles[1] <> INVALID_HANDLE_VALUE then
    FindNextChangeNotification(WaitHandles[1]);
  end;

  if WaitHandles[1] <> INVALID_HANDLE_VALUE then
   FindCloseChangeNotification(WaitHandles[1]);
end;

end.

UnitMain.pas

unit UnitMain;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, FolderMonitor;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    Edit1: TEdit;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure OnFolderChange(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  end;

var
  Form1: TForm1;
  Mon: TFolderMonitor;
  X: integer;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
 X:=0;
 Mon:=TFolderMonitor.Create('D:\Test',OnFolderChange);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
 Mon.Terminate;
 Mon.Unblock;
 Mon.WaitFor;
 Mon.Free;
end;

procedure TForm1.OnFolderChange(Sender: TObject);
begin
 inc(x);
 Memo1.Lines.Add('changed! '+IntToStr(x));
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
 Mon.Path:=Edit1.Text;
end;

end.

您有一个在多个线程之间共享的变量,一个线程修改该变量。这种情况称为数据竞争。

有些种族可能是良性的。这个不是。如果一个线程修改变量而另一个线程读取它,则可能会发生错误。因为数据类型很复杂(指向堆分配的字符数组的指针),读取线程很可能会尝试从释放的内存中读取。

对于像这样的复杂类型,无论何时访问该值都需要使用互斥锁。所有的读取和写入都必须由锁序列化。使用临界区或监视器。

为确保您永远不会执行不受保护的访问,明智的做法是在代码中强制执行此规则。例如,我的 TThreadSafe<T> 在这里描述:Generic Threadsafe Property