【问题标题】:It is safe to change variable values of a thread from the main thread?从主线程更改线程的变量值是否安全?
【发布时间】:2015-05-23 11:31:37
【问题描述】:

我编写了一个简单的组件来监视文件夹并在检测到更改时触发事件。它运作良好......显然。但我不确定一件事。有时,主线程可能需要更新被监控的路径,我不确定我是否做对了。这是关于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.

【问题讨论】:

  • 是的,让一个线程修改字符串字段同时另一个线程可以读取它是不安全的,
  • 我怎样才能让它安全?

标签: delphi thread-safety delphi-2009


【解决方案1】:

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

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

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

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

【讨论】:

  • 没有。一个(或多个)代码块的序列化意味着在任何时候只能执行该块的一个线程。强制所有访问发生在同一个线程中是一种实现序列化的方法。但是,它是一种非常低效的序列化方法。使用互斥锁可以让代码在不同的线程上执行,并且只有两个或更多线程争用该锁时才会有延迟。
  • 一般来说,强制在特定线程上执行通常仅在该代码与特定线程具有亲和性时才会执行。典型的例子是与 UI 线程有关联的 UI 代码。但是为了确保变量的线程安全共享访问,互斥通常是正确的解决方案。对于某些数据类型,可以使用原子操作(称为无锁同步),其性能优于完整的互斥锁。但这对于像字符串这样的复杂类型是不可行的。
  • 感谢您的解释,我很抱歉,因为我删除了我的评论。我正在阅读 TCriticalSection 帮助,发现所有线程都必须使用临界区以确保安全。
  • 我认为你错了。字符串赋值是用 XCHG 实现的,这使得它是线程安全的。其他线程可能会读取旧值(如果 XCHG 尚未执行)或新值(如果 XCHG 已完成),但其他线程无法读取垃圾、垃圾或“部分更新”的值。
  • @alex 不,我是对的。你错了。线程 A 读取指针。然后线程 B 销毁该指针。然后线程 A 取消引用现在无效的指针。
猜你喜欢
  • 1970-01-01
  • 2013-04-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-05
  • 2014-06-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多