Multithreading 带有计时器事件和其他多线程客户端事件的Delphi tIdTCPClient

Multithreading 带有计时器事件和其他多线程客户端事件的Delphi tIdTCPClient,multithreading,delphi,tcp,indy,ttimer,Multithreading,Delphi,Tcp,Indy,Ttimer,我们有一个使用INDY的Delphi客户机-服务器应用程序。客户端与服务器之间有一个多线程的tIdTCPClient连接。客户端“理论上”是单个线程。但实际上,客户端上有多个线程,这就是我的问题所在。例如,设想一个计时器,它每分钟启动一次以从服务器获取数据。并且考虑当用户在这个计时器事件的同时运行命令时会发生什么。事实上,我的问题是由我们的“ReportBuilder”报告工具造成的,它(令人烦恼地)坚持加载报告的每一页,这需要一段时间。该报告运行我们的“特殊”数据集,该数据集具有一种缓存机制,

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

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

所以在客户机上,我有这样的代码

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; 
过程docomand(MyIdTCPClient:tidtcplient;var数据:tMemoryStream);
变量
缓冲区:t字节;
数据大小:整数;
CommsVerTest:字符串;
开始
//写入数据
MyIdTCPClient.IOHandler.Write(DATA.Size);
MyIdTCPClient.IOHandler.Write(RawToBytes(Data.Memory^,DataSize));
//回读6字节通信测试应始终相同(即ABC123)
SetLength(缓冲区,0)//清除缓冲区
MyIdTCPClient.IOHandler.ReadBytes(缓冲区,6);
CommsVerTest:=BytesToString(缓冲区);
如果CommsVerTest'ABC123',则
引发异常。创建('Invalid Comms')//很少有这种情况
//从服务器获取结果数据
数据大小:=MyIdTCPClient.IOHandler.ReadLongInt;
Data.SetSize(DataSize)//报告线程被卡在这里
MyIdTCPClient.IOHandler.ReadBytes(缓冲区,数据大小);
结束;
现在我调试它时,在这个过程中间有两个线程时,我可以确认它是错误的。主线程在异常处停止。报告线程在同一过程中被卡在其他地方

因此,在我看来,我需要使上述过程线程安全。 我的意思是,如果用户想要做一些事情,他们只需要等到报表线程完成

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

我认为使用TThread是行不通的,因为我没有访问报表生成器中的线程的权限。我想我需要一个批判性的部分

我认为我需要创建应用程序,以便上述过程一次只能由一个线程运行。其他线程必须等待


请有人帮助理解语法。

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

procedure Write(AStream: TStream; ASize: TIdStreamSize = 0; AWriteByteCount: Boolean = False); overload; 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; 
过程docomand(MyIdTCPClient:tidtcplient;var数据:TMemoryStream);
变量
CommsVerTest:字符串;
开始
//写入数据
MyIdTCPClient.IOHandler.Write(数据,0,True);
//回读6字节通信测试应始终相同(即ABC123)
CommsVerTest:=MyIdTCPClient.IOHandler.ReadString(6);
如果通信测试为“ABC123”,则
引发异常。创建('Invalid Comms');
//从服务器获取结果数据
数据。清晰;
MyIdTCPClient.IOHandler.ReadStream(数据,-1,False);
结束;
也就是说,如果有多个线程同时向同一个套接字写入数据,或者有多个线程同时从同一个套接字读取数据,那么它们将破坏彼此的数据(甚至更糟)。您需要同步对套接字的访问,例如至少与关键部分同步。由于您对
TIdTCPClient
的多线程使用,您确实需要重新考虑您的整个客户端设计

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

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

  • 如果您的协议允许多个命令并行运行,那么您可以随时从任何线程发送命令(请确保使用关键部分以避免重叠),但不要立即等待响应。让每个发送线程继续前进并做其他事情,让读取线程在预期响应实际到达时异步通知相应的发送线程

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