【问题标题】:Delphi tIdTCPClient with timer events and other multi-threaded client side events带有计时器事件和其他多线程客户端事件的 Delphi tIdTCPClient
【发布时间】:2013-08-21 06:09:04
【问题描述】:

我们有一个使用 INDY 的 Delphi 客户端服务器应用程序。客户端与服务器的单个 tIdTCPClient 连接是多线程的。客户端“理论上”是一个单线程。但实际上客户端上有多个线程,这就是我的问题所在。例如,考虑一个每分钟触发一次以从服务器获取数据的计时器。并考虑当用户在此计时器事件的同时运行命令时会发生什么。事实上,我的问题是由我们的“报表生成器”报表工具引起的,它(令人讨厌地)坚持加载报表的每一页,这需要一段时间。该报告运行我们的“特殊”数据集,该数据集具有一次传输批量记录的缓存机制(因此多次调用服务器以获取所有数据)。同时,如果用户同时做其他事情,我们似乎得到了交叉数据。用户似乎取回了本应用于报告的数据。

顺便说一下,这个错误非常罕见,但对于拥有世界上最慢互联网的特定客户来说,这种情况要少得多(幸运的是,我现在有一个测试环境)。

所以在客户端我的代码有点像这样......

procedure DoCommand(MyIdTCPClient:tIdTCPClient; var DATA:tMemoryStream);
var
  Buffer: TBytes;
  DataSize: Integer;
  CommsVerTest: String;
begin
  //Write Data
  MyIdTCPClient.IOHandler.Write(DATA.Size);
  MyIdTCPClient.IOHandler.Write(RawToBytes(Data.Memory^,DataSize));

  //Read back 6 bytes CommsVerTest should always be the same (ie ABC123)
  SetLength(Buffer,0); //Clear out buffer
  MyIdTCPClient.IOHandler.ReadBytes(Buffer,6); 
  CommsVerTest:=BytesToString(Buffer);
  if CommsVerTest<>'ABC123' then
    raise exception.create('Invalid Comms');      //It bugs out here in rare cases

  //Get Result Data Back from Server
  DataSize:=MyIdTCPClient.IOHandler.ReadLongInt;   
  Data.SetSize(DataSize);                         //Report thread is stuck here
  MyIdTCPClient.IOHandler.ReadBytes(Buffer,DataSize);
end; 

现在,当我调试它时,我可以确认在此过程中间有两个线程时它会出错。主线程在异常处停止。并且报告线程卡在同一过程中的其他地方。

所以,在我看来,我需要使线程上面的过程安全。 我的意思是,如果用户想要做某事,他们只需要等到报告线程完成。

啊,我以为我的客户端应用程序是单线程的,用于向服务器发送数据!

我认为使用 TThread 是行不通的——因为我无法访问报表生成器中的线程。我想我需要一个 tCriticalSection。

我想我需要使应用程序一次只能由一个线程运行上述过程。其他线程必须等待。

请大家帮忙看看语法。

【问题讨论】:

    标签: multithreading delphi tcp indy ttimer


    【解决方案1】:

    TIdIOHandler 具有 Write()Read...() 重载,用于发送/接收 TStream 数据:

    procedure Write(AStream: TStream; ASize: TIdStreamSize = 0; AWriteByteCount: Boolean = False); overload; virtual;
    

    procedure ReadStream(AStream: TStream; AByteCount: TIdStreamSize = -1; AReadUntilDisconnect: Boolean = False); virtual;
    

    您不需要在发送之前将TMemoryStream 内容复制到中间TIdBytes,或者在将其复制回TStream 之前将其作为TIdBytes 接收。实际上,您显示的代码中没有任何内容需要直接使用TIdBytes

    procedure DoCommand(MyIdTCPClient: TIdTCPClient; var DATA: TMemoryStream);
    var
      CommsVerTest: String;
    begin
      //Write Data
      MyIdTCPClient.IOHandler.Write(DATA, 0, True);
    
      //Read back 6 bytes CommsVerTest should always be the same (ie ABC123)
      CommsVerTest := MyIdTCPClient.IOHandler.ReadString(6); 
      if CommsVerTest <> 'ABC123' then
        raise exception.create('Invalid Comms');
    
      //Get Result Data Back from Server
      DATA.Clear;
      MyIdTCPClient.IOHandler.ReadStream(DATA, -1, False);
    end; 
    

    话虽如此,如果您有多个线程同时写入同一个套接字,或者多个线程同时从同一个套接字读取,它们将破坏彼此的数据(或更糟)。您需要同步对套接字的访问,例如至少与关键部分同步。由于您对TIdTCPClient 的多线程使用,您确实需要重新考虑您的整体客户端设计。

    至少,使用您现有的逻辑,当您需要发送命令并读取响应时,停止计时器并等待任何待处理的数据被交换,然后再发送命令,并且不允许其他任何事情访问套接字,直到响应返回。您试图一次做太多事情而不同步所有内容以避免重叠。

    从长远来看,从单个专用线程执行所有读取然后将任何接收到的数据传递给其他线程以根据需要进行处理会更安全。但这也意味着更改您的发送逻辑以匹配。你可以:

    1. 如果您的协议允许您并行运行多个命令,那么您可以随时从任何线程发送命令(请务必使用临界区以避免重叠),但不要等待立即回复。让每个发送线程继续执行其他操作,并让读取线程在预期响应实际到达时异步通知相应的发送线程。

    2. 如果协议不允许并行命令,但您仍然需要每个发送线程等待其各自的响应,则为套接字线程提供一个线程安全队列,其他线程可以在需要时将命令推送到该队列中。然后,套接字线程可以在该队列中运行,定期发送每个命令并根据需要一次接收一个响应。每个将命令放入队列的线程都可以包含一个TEvent,以便在响应到达时发出信号,这样它们在等待时会进入有效的睡眠状态,但您保留每个线程的等待逻辑。

    【讨论】:

      【解决方案2】:

      谢谢雷米。

      TCriticalSection 解决了这个问题。我无法控制诸如 3rd 方报告生成器之类的东西。并且完全在他们自己的线程中运行报告不会有太大的不同 - 他们仍然需要共享相同的连接(我不想要或不需要并行连接)。反正程序的大部分运行在主线程中,很少有两个线程需要同时与服务器通信。

      所以 TCriticalSection 是完美的——它阻止了这个过程同时运行两次(即一个线程必须等到第一个线程完成)。很高兴 - 它工作得很好。

      现在的代码基本上是这样的:

      procedure DoCommand(
          CS:tCriticalSection; 
          MyIdTCPClient:tIdTCPClient; 
          var DATA:tMemoryStream);
      var
        Buffer: TBytes;
        DataSize: Integer;
        CommsVerTest: String;
      begin
        CS.Enter;     //enter Critical Section
        try
          //Write Data
          MyIdTCPClient.IOHandler.Write(DATA.Size);
          MyIdTCPClient.IOHandler.Write(RawToBytes(Data.Memory^,DataSize));
      
          //Read back 6 bytes CommsVerTest should always be the same (ie ABC123)
          SetLength(Buffer,0); //Clear out buffer
          MyIdTCPClient.IOHandler.ReadBytes(Buffer,6); 
          CommsVerTest:=BytesToString(Buffer);
          if CommsVerTest<>'ABC123' then
            raise exception.create('Invalid Comms');      
      
          //Get Result Data Back from Server
          DataSize:=MyIdTCPClient.IOHandler.ReadLongInt;   
          Data.SetSize(DataSize);                         
          MyIdTCPClient.IOHandler.ReadBytes(Buffer,DataSize);
        finally
          cs.Leave;
        end;
      end;
      

      【讨论】:

        猜你喜欢
        • 2019-12-09
        • 1970-01-01
        • 2023-03-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多