1

我们有一个使用 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; 

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

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

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

我认为使用 TThread 是行不通的——因为我无法访问 Report Builder 中的线程。我想我需要一个 tCriticalSection。

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

有人请帮助语法。

4

2 回答 2

3

TIdIOHandlerWrite()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内容复制到中间体,也不需要像在将其复制回 TStream 之前TIdBytes一样接收它。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在响应到达时发出信号的线程,这样它们在等待时会进入有效的睡眠状态,但您保留每个线程的等待逻辑。

于 2013-08-19T16:47:59.020 回答
0

谢谢雷米。

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;
于 2013-08-20T12:55:29.157 回答