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

时间:2013-08-19 12:32:07

标签: multithreading delphi tcp indy ttimer

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

顺便说一句,这个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; 

现在当我调试它时,我可以在此过程中间有两个线程时确认它是错误的。主线程在异常处停止。并且报告线程在相同的过程中停留在其他位置。

所以,我认为我需要使上面的过程安全。 我的意思是,如果用户想要做某事,他们只需要等到报告线程结束。

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

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

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

有人请帮助解决语法问题。

2 个答案:

答案 0 :(得分:2)

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,以便在响应到达时发出信号,这样他们在等待时就会进入有效的睡眠状态,但是你保留了每线程等待逻辑。

答案 1 :(得分:0)

谢谢雷米。

TCriticalSection解决了这个问题。我无法控制第三方报表生成器等内容。完全在他们自己的线程中运行报告不会有太大的区别 - 他们仍然需要共享相同的连接(我不想要或不需要并行连接)。无论如何,程序的大部分都在主线程中运行,并且两个线程很少需要同时与服务器通信。

所以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;