4

I've been writing unit to search for files that end with specified extensions and with ability to skip searching through specified directories. This data is contained in FExtensions and FIgnorePaths TStringList objects, respectively.

However, approximately 1 out of 10 runs, thread crashes with following exception:

thread crash exception

After debugging a bit, I isolated this line in search thread as a crash cause:

if FExtensions.IndexOf(ExtractFileExt(search_rec.Name)) <> -1 then

I've tried to do Assigned(FExtensions) check before I call IndexOf(), but that didn't eliminated the crash. If I comment this line, thread stress test works fine (creating/destroying it with 100ms intervals). I know that TStringList isn't thread safe, but I do not access FExtensions nor any other TStringList in thread anywhere out of it's scope, so concurrent access shouldn't be crash cause.

Here is the file search thread unit:

unit uFileSearchThread;

interface

uses
  Winapi.Windows, System.Classes, System.Generics.Collections;

type
  TFileSearchThread = class(TThread)
  private
    FExternalMessageHandler: HWND;
    FMsg_FSTDone           : Cardinal;

    FPath                  : String;
    FIgnorePaths           : TStringList;
    FExtensions            : TStringList;
    FFiles                 : TStringList;

    function IsIgnoreDir(const ADir: String; out AKeepIgnoreCheck: Boolean): Boolean;

  protected
    procedure Execute; override;

  public
    constructor Create(const APath: String; const AIgnorePaths: TStringList; const AAllowedExtensions: TStringList; const AExternalMessageHandler: HWND; const AMsg_FSTDone: Cardinal);
    destructor Destroy; override;

    property Path : String read FPath;
    property Files: TStringList read FFiles;

  end;

  TFileSearchThreads = TObjectList<TFileSearchThread>;

implementation

uses
  System.SysUtils, System.StrUtils;


constructor TFileSearchThread.Create(const APath: String; const AIgnorePaths: TStringList; const AAllowedExtensions: TStringList; const AExternalMessageHandler: HWND; const AMsg_FSTDone: Cardinal);
begin
  inherited Create(TRUE);

  FExternalMessageHandler := AExternalMessageHandler;
  FMsg_FSTDone := AMsg_FSTDone;

  FPath := IncludeTrailingPathDelimiter(APath);

  FIgnorePaths := TStringList.Create;
  FIgnorePaths.Assign(AIgnorePaths);

  FExtensions := TStringList.Create;
  FExtensions.Assign(AAllowedExtensions);

  FFiles := TStringList.Create;

  WriteLn(FPath, ' file search thread created.');
 end;

destructor TFileSearchThread.Destroy;
begin
  FExtensions.Free;
  FIgnorePaths.Free;

  WriteLn(FPath, ' file search thread destroyed.');

  inherited;
end;

function TFileSearchThread.IsIgnoreDir(const ADir: String; out AKeepIgnoreCheck: Boolean): Boolean;
var
  C1: Integer;
begin
  AKeepIgnoreCheck := FALSE;
  if not Assigned(FIgnorePaths) then
    Exit(FALSE);

  for C1 := 0 to FIgnorePaths.Count - 1 do
    if AnsiStartsText(FIgnorePaths[C1], ADir) then
      Exit(TRUE)
    else
      if not AKeepIgnoreCheck then
        AKeepIgnoreCheck := AnsiStartsText(ADir, FIgnorePaths[C1]);

  Exit(FALSE);
end;

procedure TFileSearchThread.Execute;
var
  search_rec      : TSearchRec;
  dirs            : TStringList;
  dirs_nocheck    : TStringList;
  dir             : String;
  ignore_check    : Boolean;
  ignore_check_tmp: Boolean;
  newdir          : String;
begin
  dirs := TStringList.Create;
  try
    dirs_nocheck := TStringList.Create;
    try
      dirs.Add(FPath);

      while (not Terminated) and
            ((dirs.Count > 0) or (dirs_nocheck.Count > 0)) do
      begin
        ignore_check := dirs.Count > 0;
        if ignore_check then
        begin
          dir := dirs[0];
          dirs.Delete(0);
        end
        else
        begin
          dir := dirs_nocheck[0];
          dirs_nocheck.Delete(0);
        end;

        if (not ignore_check) or
           (not IsIgnoreDir(LowerCase(dir), ignore_check)) then
          if FindFirst(dir + '*', faAnyFile, search_rec) = 0 then
          try
            repeat
              if (search_rec.Attr and faDirectory) = 0 then
              begin
                if FExtensions.IndexOf(ExtractFileExt(search_rec.Name)) <> -1 then // crashes here
                  FFiles.Add(dir + search_rec.Name);
              end
              else
                if (search_rec.Name <> '.') and (search_rec.Name <> '..') then
                begin
                  newdir := dir + search_rec.Name + '\';
                  if not ignore_check then
                    dirs_nocheck.Add(newdir)
                  else
                    if not IsIgnoreDir(LowerCase(newdir), ignore_check_tmp) then
                      if ignore_check_tmp then
                        dirs.Add(newdir)
                      else
                        dirs_nocheck.Add(newdir);
                end;
            until (Terminated) or (FindNext(search_rec) <> 0);
          finally
            FindClose(search_rec);
          end;
      end;
    finally
      dirs_nocheck.Free;
    end;
  finally
    dirs.Free;
  end;

  PostMessage(FExternalMessageHandler, FMsg_FSTDone, NativeUInt(pointer(self)), 0);
end;

end.

(I know that I don't free FFiles in destructor, but that is because I want to avoid data duplication, so I pass it after thread destruction to another object that keeps using it)

And procedure that creates the thread:

procedure CreateFileSearchThread(const APath: String);
const
  {$I ignore_dirs.inc}
  {$I allowed_extensions.inc}
var
  ignore_dirs_list, allowed_exts_list: TStringList;
  file_search_thread                 : TFileSearchThread;
  C1                                 : Integer;
begin
  ignore_dirs_list := TStringList.Create;
  try
    ignore_dirs_list.Sorted := TRUE;
    ignore_dirs_list.CaseSensitive := FALSE;
    ignore_dirs_list.Duplicates := dupIgnore;

    for C1 := Low(IGNORE_DIRS) to High(IGNORE_DIRS) do
      ignore_dirs_list.Add(LowerCase(ExpandEnvStrings(IGNORE_DIRS[C1])));

    allowed_exts_list := TStringList.Create;
    try
      allowed_exts_list.Sorted := TRUE;
      allowed_exts_list.CaseSensitive := FALSE;
      allowed_exts_list.Duplicates := dupIgnore;

      for C1 := Low(ALLOWED_EXTS) to High(ALLOWED_EXTS) do
        allowed_exts_list.Add('.' + ALLOWED_EXTS[C1]);

      file_search_thread := TFileSearchThread.Create(APath, ignore_dirs_list, allowed_exts_list, FMessageHandler, FMsg_FSTDone);
      FFileSearchThreads.Add(file_search_thread);
      file_search_thread.Start;
    finally
      allowed_exts_list.Free;
    end;
  finally
    ignore_dirs_list.Free;
  end;
end;

I destroy thread simply by calling FFileSearchThreads.Free, which then should free it's objects, since OwnObjects is set to TRUE. FFileSearchThreads is of TObjectList<TFileSearchThread> type.

4

2 回答 2

4

我只是通过调用 FFileSearchThreads.Free 来销毁线程,然后它应该释放它的对象,因为 OwnObjects 设置为 TRUE。FFileSearchThreads 属于 TObjectList 类型。

等一下。你告诉你的线程之前Terminate()WaitFor()他们完成,是吗?如果没有,那么你真的应该这样做!

线程不仅包含存储在 TThread 实例中的数据。它分配一堆与操作系统线程对象相关联的系统资源,该对象表示单个执行流/上下文。必须正确释放这些资源并停止执行,然后才能在内部 OS 对象周围释放()Delphi 对象。

值得考虑FreeOnTerminate := TRUE的是,基本上让线程单独完成清理工作。你仍然负责启动这个过程,通常是通过设置一个共享的全局标志或TEvent实例或类似的东西。这样您就可以解耦事物并摆脱线程列表。两种方法都有其优点和缺点。

于 2013-10-18T21:20:33.020 回答
1

为了完整起见,这里是发生了什么:

  1. Execute方法是使用andFIgnorePaths对象FExtensions
  2. Execute析构函数会在仍在飞行中时销毁这些对象。
  3. 然后Execute在它们被释放后访问这些对象。繁荣!

查看线程的析构函数:

destructor TFileSearchThread.Destroy;
begin
  FExtensions.Free;  
  // Execute is still active at this point

  FIgnorePaths.Free; 
  // and still active here

  inherited;      
  // this calls Terminate and WaitFor, and that brings matters to a close, 
  // but not before the thread has opportunity to access the objects which
  // you just destroyed
end;

您需要重新设计事物以确保线程在销毁后不使用任何对象。

于 2013-10-18T21:49:08.973 回答