附加到先前由另一个线程创建的数据集

时间:2013-12-28 12:25:10

标签: mysql database multithreading delphi dataset

这是一个类似于this one的问题,但背景不同。

编译器:Delphi 2010,很快Delphi XE5。

我构建了一个很好的应用程序,通过ZEOS components管理远程MySQL服务器上的数据。 由于连接可能失败并且SQL很慢,我使用了整齐的OmniThreadLibrary来创建一个SQL服务器监视程序,并卸载了许多加载到线程的“只读”表。 截至目前,我在主窗体显示之前手动创建了三个数据模块,每个模块都有独立的TZConnection和一些链接到同一数据模块TZConnection的TZReadOnlyQuery组件。每个线程从其自身内部实例化其相关数据模块,然后执行查询。

看门狗工作得很好,但我对第二部分有疑问,那就是“只读”表格线程。查询已经工作但我还没有在主应用程序业务代码中使用他们的结果,我必须在其他表上插入和更新数据。

在我的计划中,我在主应用程序甚至连接它们之前读取并加载了所有这些“只读”数据集(整个线程间状态机已经完成)。理论上应该没有并发问题,因为“只读”表线程已完成其任务并且现在处于空闲状态。 但我不知道如果此时我将控件或其他数据集/数据源/从主窗体连接到空闲线程数据模块会发生什么。

我会搞砸,因为主要形式TZSession与线程数据模块不一样吗?我会变得罕见吗?只有在交付应用程序后才会发现令人讨厌的访问冲突(当然!)。基本上我应该使用什么样的信心或预防措施来访问在另一个线程中创建的查询组件,假设只有主应用程序执行它并且仅用于读取数据?它甚至可能/健康吗?或者我错过了一些“最佳实践”的方式吗?

提前致谢。

1 个答案:

答案 0 :(得分:3)

我将发布我是如何做到的。它会"简洁" (它仍然是一个巨大的文字墙!)由于时间不够,如果你发现一些太模糊的东西随意问。我不假装既没有写出最好也没有最好的格式化代码。只需将它作为起点。

问题域的简要概述:拥有一个预先打开的软件"使用线程启动时的表。这使软件保持响应,甚至执行其他非数据库相关的启动任务。 我已经测试了这个程序1个月了,所使用的数据库组件足以证明它不仅仅是一个玩具演示"准备在添加第3个数据集时中断。

成分:

如上所述:Delphi 2010+(我现在在RAD Studio XE5 Ultimate中运行它)但可能适用于早期版本。我只是没有测试它们。

  • ZEOS库,7及以上应该可以使用,包括最新的7.2 alpha。
  • OmniThreadLibrary
  • 在我的情况下,我也使用了JVCL,但这仅仅是因为我需要一些基本组件不提供的特定数据源事件。


IDE,非代码部分

  • 创建2个将托管数据库组件的数据模块:一个用于" preload&#34 ;,螺纹部分,另一个用于"运行时",读写部分将基本上是程序用户用来执行任务的那个。

  • 创建1个单位来存储线程工作者代码

  • 创建一个或多个构成您的申请的表格。

这两个数据模块如下所示:

预加载模块,由工作线程管理。

Preaload data module

主数据库模块的一部分。它包括预加载模块无法预加载的其他几个数据集。这些通常是"动态"数据集,其查询直接受与用户交互的影响。 主数据库模块的组件作为预加载模块的复制和粘贴启动,因此准备两者都不需要两倍的时间。

Main database data module

预加载和主数据库模块都附带ConnectToDatabase和DisconnectFromDatabase过程,这些过程执行使系统启动并运行所需的所有步骤。

非常重要!

  • 预加载模块执行" true"在单独的线程中查询并填写其相关的TClientDataSets。其组件没有附加事件。我只把它们用作盲人"静电"数据容器。
  • 主数据库模块只会"附加"到预装模块组件。

在示例中:而preload模块cdsProducts ClientDataSet执行"真正的数据库查询"用

cdsProduct => dspProduct => qryProduct

链,主数据库模块cdsProduct只需要预加载模块的cdsProduct数据而不执行任何查询(否则,该点是什么,执行两次查询?)

你看到的反直觉性,主数据库模块cdsProduct还附带了一个链接的TDataSetProvider和查询组件。为什么?因为我用它们来修改数据。

也就是说,我们有三个计划阶段:

  1. 启动,其中预加载数据模块执行查询(在线程中)并执行查询。没有管理任何事件,都是只读的。

  2. 运行启动阶段,主数据库模块(在VCL线程中)将1中收集的数据复制到其ClientDataSet中。

  3. 运行阶段,用户与主数据库模块的ClientDataSets交互。当他们需要保存数据时(并且仅在那时),主数据库模块的DataSetProviders和查询被使用。他们只写。

  4. 我本可以跳过整个ClientDataSet =>提供者=>其中一些ClientDataSets的查询链,但大多数都需要一些大量的数据处理,必须手动更新许多连接表等等,所以我只使用了完整的堆栈。


    代码部分

    让我们了解更多细节。我无法发布所有内容,因为它是商业应用程序,因此我只会粘贴一些重要的代码段。


    螺纹预载数据模块

    procedure TModDBPreload.ConnectToDatabase;
    begin
        dbcEShop.Connect;
        SendStatusMessage('Loading languages archive');
        qryLanguage.Open;
        qryLanguage.First;
        SearchOptions := [loCaseInsensitive];
        ModApplicationCommon.ApplicationLocaleInfo.Lock;
    
        ...
    
        try
            ...
    
             // All the queries parameters needing a language id need to be assigned to the locked LocaleInfo object
            qryGeoZone.Params.ParamByName('language_id').AsInteger := ModApplicationCommon.ApplicationLocaleInfo.LocaleIDForQueries;
            cdsGeoZones.Params.ParamByName('language_id').AsInteger := ModApplicationCommon.ApplicationLocaleInfo.LocaleIDForQueries;
    
            ...
    
        finally
            ModApplicationCommon.ApplicationLocaleInfo.Unlock;
        end;
    
      SendStatusMessage('Loading countries archive');
      cdsGeoZones.Open;
          cdsGeoZones.First;
      SendStatusMessage('Loading currencies archive');
      qryCurrency.Open;
      qryCurrency.First;
      Sleep(100);
      SendStatusMessage('Loading products archive');
      cdsProduct.Open;
      cdsProduct.First;
      ...
    end;
    

    上面的代码片段可以使用很多解释。特别是:

    SendStatusMessage('Loading languages archive');
    

    是一个发送最终用户友好更新字符串的线程,以显示在状态行上。当然,状态行由主VCL线程管理。怎么做?我稍后会再说。

    qryLanguage.Open;
    qryLanguage.First;
    ...
    cdsGeoZones.Open;
    cdsGeoZones.First;
    

    并非所有数据集都需要在整个应用程序持续时间内进行管理。只有那些需要的东西由ClientDataSets管理。 "第一"呼叫发生是因为我不知道服务器后端是否会发生变化。某些数据库驱动程序,DLL,(特别是)ODBC连接器等在打开期间但在第一个光标操作时不执行实际的重载。 因此我确保它发生,即使当前的驱动程序并不严格需要它。 Sleep(100)允许用户和开发人员在打开小表时看到消息。当然,可以在软件最终结束后删除。

    Lock, try / finally条款等是为了提醒您,我们处于一个线程中,并且通过一些预防措施可以最好地访问某些资源。在这种特定情况下,我们有其他线程(与本文无关,因此未涵盖),因此我们必须保护一些数据结构。 特别是,我已经"借了"基本的Delphi线程安全列表锁定机制paradygm所以方法名也是一样的。


    基于OmniThreadLibrary的预加载模块线程工作者

    以下是最相关/教学代码:

    type
      TDBPreloadWorker = class(TOmniWorker)
      protected
        ThreadModDatabase : TModDBPreload;
        FStatusString : string;
      public
        constructor Create;
        function Initialize : boolean; override;
        procedure Cleanup; override;
        procedure SendStatusMessage(anID : Word; aValue : string = ''); overload;
        procedure SendStatusMessage(aValue : string); overload;
        procedure DisconnectFromDatabase;
    
        procedure OMSendMessage(var msg: TOmniMessage); message MSG_SEND_MESSAGE;
        procedure OMDisconnectFromDatabase(var msg: TOmniMessage); message MSG_DISCONNECT_FROM_DATABASE;
        procedure OMUpdateStateMachine(var msg: TOmniMessage); message MSG_UPDATE_STATE_MACHINE;
      end;
    ...
    
    constructor TDBPreloadWorker.Create;
    begin
      Inherited;
      FStatusString := 'Connecting to server...';
      ThreadModDatabase := Nil;
    end;
    
    function TDBPreloadWorker.Initialize : boolean;
    begin
      ThreadModDatabase := TModDBPreload.Create(Nil);
      ModDBPreload := ThreadModDatabase;
      ThreadModDatabase.DBPreloadWorker := Self;
      DisconnectFromDatabase; // In case of leftover Active := true from designing the software
      Result := true;
    end;
    
    procedure TDBPreloadWorker.Cleanup;
    begin
      DisconnectFromDatabase;
      ThreadModDatabase.Free;
      ThreadModDatabase := Nil;
    end;
    
    procedure TDBPreloadWorker.SendStatusMessage(anID : Word; aValue : string);
    begin
      FStatusString := aValue; // Stored in case the main application polls a status update
      Task.Comm.Send(anID, aValue);
    end;
    
    procedure TDBPreloadWorker.SendStatusMessage(aValue : string);
    begin
      SendStatusMessage(MSG_GENERAL_RESPONSE, aValue);
    end;
    
    procedure TDBPreloadWorker.DisconnectFromDatabase;
    begin
      if Assigned(ThreadModDatabase) then
        ThreadModDatabase.DisconnectFromDatabase;
    end;
    
    procedure TDBPreloadWorker.OMSendMessage(var msg: TOmniMessage);
    begin
      Task.Comm.Send(MSG_GENERAL_RESPONSE, FStatusString);
    end;
    
    procedure TDBPreloadWorker.OMDisconnectFromDatabase(var msg: TOmniMessage);
    begin
      ...
      DisconnectFromDatabase;
    end;
    
    procedure TDBPreloadWorker.OMSendMessage(var msg: TOmniMessage);
    begin
      Task.Comm.Send(MSG_GENERAL_RESPONSE, FStatusString);
    end;
    
    procedure TDBPreloadWorker.OMUpdateStateMachine(var msg: TOmniMessage);
    begin
      Task.Comm.Send(MSG_GENERAL_RESPONSE, FStatusString); // Needed to show the pre-loaded status
    
      if Assigned(ThreadModDatabase) then
      begin
        try
          ThreadModDatabase.ConnectToDatabase;
          SendStatusMessage('Reading database tables...');
    
          if not ThreadModDatabase.QueryExecute then
          begin
            raise Exception.Create('Consistency check: the database does not return the expected values');
          end;
    
          SendStatusMessage(MSG_SUCCESS, 'Tables have been succesfully read');
          SendStatusMessage(MSG_TASK_COMPLETED);
    
        except
          On E : Exception do
          begin
            DisconnectFromDatabase;
            SendStatusMessage(MSG_TASK_FAILURE, E.Message);
          end;
        end;
      end;
    end;
    

    有些代码值得进一步解释:

    function TDBPreloadWorker.Initialize : boolean;
    

    创建预加载数据模块。也就是说,一切都在线程的上下文中自包含,并且不与其他人冲突。

    procedure TDBPreloadWorker.SendStatusMessage(anID : Word; aValue : string);
    

    这是通过OmniThreadLibrary向主VCL线程发送消息(顺便说一下,它不限于字符串)。

    procedure TDBPreloadWorker.OMUpdateStateMachine(var msg: TOmniMessage);
    
    这是主要的预装数据模块初始化管理代码。它与VCL主线程进行握手,基本上作为我在程序中实现的状态机之一。

    对于那些想知道所有这些常量来自何处的人:它们在所有线程相关类包含的单独文件中声明。它们很简单,可以自由选择整数:

    const
      MSG_GENERAL_RESPONSE         = 0;
      MSG_SEND_MESSAGE             = 1;
      MSG_SHUTDOWN                 = 2;
      MSG_SUCCESS                  = $20;
      MSG_ABORT                    = $30;
      MSG_RETRY                    = $31;
      MSG_TASK_COMPLETED           = $40;
      MSG_FAILURE                  = $8020;
      MSG_ABORTED                  = $8030;
      MSG_TASK_FAILURE             = $8040;
      MSG_UPDATE_STATE_MACHINE     = 9;
      MSG_TIMER_1                  = 10;
      MSG_DISCONNECT_FROM_DATABASE = 99;
    


    主表单侧预加载管理代码

    在程序启动时会生成各种线程。 TOmniEventMonitor的OnTaskMessage事件指向:

    procedure TFrmMain.monDBPreloadTaskMessage(const task: IOmniTaskControl;
      const msg: TOmniMessage);
    var
      MessageString : string;
      ComponentsNewState : boolean;
    
    begin
      MessageString := msg.MsgData.AsString;
    
      if Length(MessageString) > 0 then
        UpdateStatusBar(MessageString);
    
      if task = FDBPreloadWorkerControl then
      begin
        if (msg.MsgID = MSG_TASK_COMPLETED) or (msg.MsgID = MSG_TASK_FAILURE) then
        begin
          ComponentsNewState := (msg.MsgID = MSG_TASK_COMPLETED);
    
          // Unlike for the watchdog, the preload thread is not terminated
          // The data is needed by the program till its end
          // DBPreloadTerminate;
    
          // Lets the main database queries be started
          DBPreloadSuccess := (msg.MsgID = MSG_TASK_COMPLETED);
          MainViewEnabled := ComponentsNewState;
    
          if msg.MsgID = MSG_TASK_FAILURE then
          begin
            if MessageDlg('Unable to load the data tables from the database server', mtError, [mbRetry, mbAbort], 0) = mrAbort then
              Close
            else
              // Reinitialize the preload thread.
              ...
          end;
        end;
      end;
    end;
    

    这是一个非常简单的程序,最后调用它来更新主窗体的状态栏:

    procedure TFrmMain.UpdateStatusBar(Value : string);
    begin
      pnlStatusBar.SimpleText := Value;
      pnlStatusBar.Update;
      Application.ProcessMessages;
    end;
    


    主数据库模块管理代码

    最后但并非最不重要的,以下是如何实际"附加"到预装数据模块ClientDataSets。从主表单调用此代码,基本完成应用程序的基础!

    procedure TModDatabase.ConnectToDatabase;
    
      procedure ConnectDataSet(CDS : TClientDataSet; PreloadDataSet : TClientDataSet; RuntimeDataSet : TZAbstractRODataset; SetLanguage : boolean = false);
      begin
       // Only required by datasets needing a locale_id parameter
        if (SetLanguage) then
        begin
          CDS.Params.ParamByName('language_id').AsInteger := ModApplicationCommon.ApplicationLocaleInfo.LocaleIDForQueries;
          RuntimeDataSet.ParamByName('language_id').AsInteger := ModApplicationCommon.ApplicationLocaleInfo.LocaleIDForQueries;
        end;
    
        CDS.Data := PreloadDataSet.Data;
        CDS.Active := true;
      end;
    
    begin
      DisconnectFromDatabase;
      dbcEShop.Connect;
    
      UpdateStatusBar('Setting up products archive');
      ConnectDataSet(cdsProduct, ModDBPreload.cdsProduct, qryProduct, true);
      UpdateStatusBar('Setting up products options archive');
      ConnectDataSet(cdsProductOption, ModDBPreload.cdsProductOption, qryProductOption);
      UpdateStatusBar('Setting up options archive');
      ConnectDataSet(cdsOption, ModDBPreload.cdsOption, qryOption);
      UpdateStatusBar('Setting up options descriptions archive');
      ConnectDataSet(cdsOptionDescription, ModDBPreload.cdsOptionDescription, qryOptionDescription, true);
      ...
    


    我希望发布足够的信息来概述整个过程。请随意提出任何问题,对不起,我会用英语作为第四语言。