如何在Thread的执行中使用Dictionary / StringList - delphi

时间:2015-04-06 22:00:20

标签: multithreading delphi dictionary

我有一个线程类TValidateInvoiceThread:

type
  TValidateInvoiceThread = class(TThread)
    private
      FData: TValidationData;
      FInvoice: TInvoice; // Do NOT free
      FPreProcessing: Boolean;

      procedure ValidateInvoice;
    protected
      procedure Execute; override;
    public
      constructor Create(const objData: TValidationData; const bPreProcessing: Boolean);

      destructor Destroy; override;
  end;

constructor TValidateInvoiceThread.Create(const objData: TValidationData;
      const bPreProcessing: Boolean);
    var
      objValidatorCache: TValidationCache;
    begin
      inherited Create(False);
      FData := objData;
      objValidatorCache := FData.Caches.Items['TInvAccountValidator'];
    end;

    destructor TValidateInvoiceThread.Destroy;
    begin
      FreeAndNil(FData);
      inherited;
    end;

    procedure TValidateInvoiceThread.Execute;
    begin
      inherited;
      ValidateInvoice;
    end;

    procedure TValidateInvoiceThread.ValidateInvoice;
    var
      objValidatorCache: TValidationCache;
    begin
      objValidatorCache := FData.Caches.Items['TInvAccountValidator'];
    end;

我在另一个类

中创建这个线程
    procedure TInvValidators.ValidateInvoiceUsingThread(
      const nThreadIndex: Integer;
      const objValidatorCaches: TObjectDictionary<String, TValidationCache>;
      const nInvoiceIndex: Integer; const bUseThread, bPreProcessing: Boolean);
begin
  objValidationData := TValidationData.Create(FConnection, FAllInvoices, FAllInvoices[nInvoiceIndex], bUseThread);

      objValidationData.Caches := objValidatorCaches;
      objThread := TValidateInvoiceThread.Create(objValidationData, bPreProcessing);
      FThreadArray[nThreadIndex] := objThread;
      FHandleArray[nThreadIndex]:= FThreadArray[nThreadIndex].Handle;

end;

然后我执行它

  rWait:= WaitForMultipleObjects(FThreadsRunning, @FHandleArray, True, 100);

注意我已经删除了一些代码,试图让它更容易跟随

问题是我的词典变得腐败

如果我在构造函数中放置一个断点,那么一切都很好

但是,在Execute方法的第一行中,字典现在已损坏。

字典本身是类

的全局变量

我是否需要做一些特别的事情以允许我在线程中使用字典?

我也遇到了与字符串列表相同的问题

编辑 - 按要求提供其他信息

TInvValidators包含我的词典

  TInvValidators = class(TSTCListBase)
  private
    FThreadArray  : Array[1..nMaxThreads]  of TValidateInvoiceThread;
    FHandleArray  : Array[1..nMaxThreads]  of THandle;
    FThreadsRunning: Integer;  // total number of supposedly running threads

    FValidationList: TObjectDictionary<String, TObject>;
  end;

procedure TInvValidators.Validate(
  const Phase: TValidationPhase;
  const objInvoices: TInvoices;
  const ReValidate: TRevalidateInvoices;
  const IDs: TList<Integer>;
  const objConnection: TSTCConnection;
  const ValidatorCount: Integer);
var
  InvoiceIndex:   Integer;
  i     : Integer;
  rWait : Cardinal;
  Flags: DWORD;     // dummy variable used in a call to find out if a thread handle is valid
  nThreadIndex: Integer;

  procedure ValidateInvoiceRange(const nStartInvoiceID, nEndInvoiceID: Integer);
  var
    InvoiceIndex: Integer;
    I: Integer;
  begin
    nThreadIndex := 1;

    for InvoiceIndex := nStartInvoiceID - 1 to nEndInvoiceID - 1 do
    begin
      if InvoiceIndex >= objInvoices.Count then
        Break;

      objInvoice := objInvoices[InvoiceIndex];
      ValidateInvoiceUsingThread(nThreadIndex, FValidatorCaches, InvoiceIndex, bUseThread, False);
      Inc(nThreadIndex);

      if nThreadIndex > nMaxThreads then
        Break;
    end;

    FThreadsRunning := nMaxThreads;

    repeat
      rWait:= WaitForMultipleObjects(FThreadsRunning, @FHandleArray, True, 100);

      case rWait of

        // one of the threads satisfied the wait, remove its handle
        WAIT_OBJECT_0..WAIT_OBJECT_0 + nMaxThreads - 1: RemoveHandle(rWait + 1);

        // at least one handle has become invalid outside the wait call,
        // or more than one thread finished during the previous wait,
        // find and remove them
        WAIT_FAILED:
          begin
            if GetLastError = ERROR_INVALID_HANDLE then
            begin
              for i := FThreadsRunning downto 1 do
                if not GetHandleInformation(FHandleArray[i], Flags) then // is handle valid?
                  RemoveHandle(i);
            end
            else
              // the wait failed because of something other than an invalid handle
              RaiseLastOSError;
          end;

        // all remaining threads continue running, process messages and loop.
        // don't process messages if the wait returned WAIT_FAILED since we didn't wait at all
        // likewise WAIT_OBJECT_... may return soon
        WAIT_TIMEOUT: Application.ProcessMessages;
      end;

    until FThreadsRunning = 0;  // no more valid thread handles, we're done
  end;

begin
  try
    FValidatorCaches := TObjectDictionary<String, TValidationCache>.Create([doOwnsValues]);

    for nValidatorIndex := 0 to Count - 1 do
  begin
    objValidator := Items[nValidatorIndex];
    objCache := TValidationCache.Create(objInvoices);
    FValidatorCaches.Add(objValidator.ClassName, objCache);
    objValidator.PrepareCache(objCache, FConnection, objInvoices[0].UtilityType);
   end;

    nStart := 1;
    nEnd := nMaxThreads;
    while nStart <= objInvoices.Count do
    begin
      ValidateInvoiceRange(nStart, nEnd);

      Inc(nStart, nMaxThreads);
      Inc(nEnd, nMaxThreads);
    end;

  finally
    FreeAndNil(FMeterDetailCache);
  end;
end;

如果我删除重复,直到只留下WaitForMultipleObjects,我仍然会遇到很多错误

你可以在这里看到我正在处理不超过nMaxThreads(10)的块的发票

当我恢复重复直到循环时,它在我的虚拟机上运行,​​但随后在我的主机(具有更多可用内存)上违反了访问权限

1 个答案:

答案 0 :(得分:3)

在我提供如何解决问题的指导之前,我会给你一个非常重要的提示。

  

在尝试使多线程实现正常工作之前,首先确保您的代码单线程。关键是多线程代码增加了一个全新的复杂层。在您的代码在单个线程中正常工作之前,它无法在多个线程中执行此操作。额外的复杂性使得修复非常困难。

您可能认为自己有一个可行的单线程解决方案,但我发现您的代码中存在错误,这意味着您仍然存在大量资源管理错误。这是一个只有相关行的例子,以及解释错误的评论:

begin
  try //try/finally is used for resource protection, in order to protect a
      //resource correctly, it should be allocated **before** the try.
    FValidatorCaches := TObjectDictionary<String, TValidationCache>.Create([doOwnsValues]);

  finally
    //However, in the finally you're destroying something completely
    //different. In fact, there are no other references to FMeterDetailCache
    //anywhere else in the code you've shown. This strongly implies an
    //error in your resource protection.
    FreeAndNil(FMeterDetailCache);
  end;
end;

无法使用字典的原因

你说的是:&#34;在Execute方法的第一行,字典现在已经损坏&#34;。

首先,我相当确定你的词典并没有真正“腐败”#34; “&#34;腐败&#34;暗示它在那里,但其内部数据无效导致行为不一致。当Execute方法想要使用字典时,它很可能已被破坏。因此,您的线程基本上指向用于拥有字典的内存区域,但它根本不再存在。 (即没有&#34;腐败&#34;)

  

SIDE NOTE 由于您有多个线程共享同一个字典,因此您的字典可能会真正损坏。如果不同的线程同时导致字典的任何内部更改,它很容易变得腐败。但是,假设您的线程都将字典视为只读,则需要使用内存覆盖来破坏它。

因此,让我们关注可能导致您的字典在线程使用之前被销毁的内容。 注意我无法在提供的代码中看到任何内容,但有两种可能:

  • 你的主线程在子线程使用之前销毁字典。
  • 你的一个子线程一破坏就会破坏它,导致所有其他线程无法使用它。

在第一种情况下,这将发生如下:

Main Thread: ......C......D........
Child Thread       ---------S......

. = code being executed
C = child thread created
- = child thread exists, but isn't doing anything yet
S = OS has started the child thread
D = main thread destroys dictionary

上述观点是,即使在子线程开始运行之前,主线程也很容易忘记它决定销毁字典的那一点。

至于第二种可能性,这取决于TValidationData的析构函数内发生的事情。由于您还没有显示该代码,只有您知道答案。

调试以查明问题

假设字典过早被破坏,稍微调试可以快速查明字典被破坏的位置/原因。从您的问题来看,您似乎已经完成了一些调试,所以我假设您可以顺利执行以下步骤:

  • 在字典的析构函数的第一行放置一个断点。
  • 运行您的代码。
  • 如果在到达字典的析构函数之前到达Execute,则该主题应该仍然可以使用该字典。
  • 如果您在到达Execute之前到达字典的析构函数,那么您只需要检查导致对象销毁的调用顺序。

在内存覆盖的情况下进行调试

对内存覆盖的可能性保持开放态度......调试起来有点棘手。但如果您可以始终如一地重现问题,则应该可以进行调试。

  • 在线程的析构函数中放置一个断点。
  • 运行应用
  • 当您到达上述断点时,按 Ctrl + F7 并评估@FData.Caches,找到字典的地址。
  • 现在添加数据断点(使用“断点”窗口的下拉列表),以获取地址和字典大小。
  • 继续运行,应用程序将在数据更改时暂停。
  • 再次检查调用堆栈以确定原因。

结束

您有许多问题和陈述意味着对线程之间共享数据(字典/字符串列表)的误解。我会尝试覆盖这里的那些。

  • 在线程中使用Dictionary / StringList没有什么特别需要。它与将其传递给任何其他对象基本相同。只需确保字典/字符串列表不会过早被破坏。
  • 也就是说,无论何时共享数据,您都需要了解竞争条件&#34;的可能性。即一个线程试图在另一个线程忙于修改它的同时访问共享数据。如果没有线程正在修改数据,则无需担心。但是只要任何线程能够修改数据,就需要进行访问&#34;线程安全&#34;。 (有很多方法可以做到这一点,请搜索关于SO的现有问题。)
  • 你提到:&#34; 字典本身是类的全局变量&#34;。你的术语不正确。 全局变量是在单元级别声明的,可在任何地方访问。只需说字典是该类的成员或字段就足够了。在处理&#34;全局&#34;时,需要担心的事情有很多不同;所以最好避免任何混淆。
  • 您可能想重新考虑初始化线程的方式。有些原因导致FHandleArray的某些条目无法初始化。你还好吗?
  • 您在具有更多可用内存的计算机上提到AV。注意:内存量不相关。如果你在32位模式下运行,在任何情况下你都无法访问超过4 GB。

最后,特别提一下:

  

使用多个线程执行字典查找效率极低。字典查找是O(1)操作。线程的开销几乎肯定会让你失望,除非你打算在字典查找之外做大量的处理。

PS - (不是那么大)错误

我在你的代码中注意到以下内容,这是一个错误。

procedure TValidateInvoiceThread.Execute;
begin
  inherited;
  ValidateInvoice;
end;

TThread.Execute方法抽象,这意味着没有实现。尝试调用抽象方法将触发EAbstractError。幸运的是,正如LU RD指出的那样,编译器能够通过不编译行来保护您。即便如此,在此处不调用继承会更准确。

注意:一般来说,被覆盖的方法并不总是需要调用继承的。您应该明确知道继承正在为您做什么,并决定是否在逐个案例的基础上调用它。不要仅仅因为您要覆盖虚拟方法而进入自动驾驶模式调用继承