TIdServer,再次关于同步

时间:2013-01-05 15:15:19

标签: synchronization indy

根据Remy Lebeau的几个问题和几乎令人满意的答案(再次感谢你)我尝试将代码组合起来用于我的应用程序。 有几个方面让我不清楚。当你看下面的代码时:

  • 当我使用Button3Click程序将BroadCast从GUI发送到连接的客户端时 - 这是正确的方法(我的意思是:它是否安全)?
  • 我可以输入类似于DoSomethingSafe方法的代码,我在其中创建与DB的连接,对其执行某些操作,并关闭与DB的连接?这样安全吗?
  • 为什么我的应用程序冻结时有超过20个clietns并且我想通过使用它的active来停止工作服务器:= false(button2.click方法)?
  • 我可以在TCliContext.ProccessMsg中使用TCliContext.BroadcastMessage,只是在没有任何同步的情况下调用它吗?
  • 我可以在OnConnect方法中读取(Connection.IOHandler.ReadLn())(我想读取带有登录数据的行并在DB中检查它然后当它不正确时立即断开连接?
  • 我在某处读过,使用IdSync有时会有危险(如果在使用它时出现问题),因此我有一个问题:什么是更好的解决方案来达到全局变量或VCL对象?

我的示例代码如下:

type
  TCliContext = class(TIdServerContext)
  private
    Who: String;
    Queue: TIdThreadSafeStringList;

    Activity_time: TDateTime;
    Heartbeat_time: TDateTime;

    InnerMessage: String;

    procedure BroadcastMessage(const ABuffer: String);
    procedure SendMessageTo(const ADestUser: String; const ABuffer: String);

  public
    constructor Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TThreadList = nil); override;
    destructor Destroy; override;

    procedure ProccessMsg;
    procedure DoSomethingSafe;
    procedure info_about_start_connection;
  end;

procedure TCliContext.BroadcastMessage(const ABuffer: String);
var
  cList: TList;
  Count: Integer;
  CliContext: TCliContext;
begin
  cList := Server.Contexts.LockList;
  try
    for Count := 0 to cList.Count - 1 do
    begin
      CliContext := TCliContext(cList[Count]);
      if CliContext <> Self then
        CliContext.Queue.Add(ABuffer);
    end;
  finally
    Server.Contexts.UnlockList;
  end;
end;

procedure TCliContext.SendMessageTo(const ADestUser: String;
  const ABuffer: String);
var
  cList: TList;
  Count: Integer;
  CliContext: TCliContext;
begin
  cList := Server.Contexts.LockList;
  try
    for Count := 0 to cList.Count - 1 do
    begin
      CliContext := TCliContext(cList[Count]);
      if CliContext.Who = ADestUser then
      begin
        CliContext.Queue.Add(ABuffer);
        Break;
      end;
    end;
  finally
    Server.Contexts.UnlockList;
  end;
end;

constructor TCliContext.Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TThreadList = nil);
begin
  // inherited Create(AConnection, AYarn, AList);
  inherited;
  Queue := TIdThreadSafeStringList.Create;
end;

destructor TCliContext.Destroy;
begin
  Queue.Free;
  inherited;
end;

procedure TCliContext.ProccessMsg;
begin
  InnerMessage := Connection.IOHandler.ReadLn();
  TIdSync.SynchronizeMethod(DoSomethingSafe);
  // is it ok?
end;

procedure TCliContext.info_about_start_connection;
begin
  MainForm.Memo1.Lines.Add('connected');
end;

procedure TCliContext.DoSomethingSafe;
begin
  MainForm.Memo1.Lines.Add(InnerMessage);
end;

使用GUI

编写代码
procedure TMainForm.BroadcastMessage(Message: string);
var
  cList: TList;
  Count: Integer;
begin
  cList := IdTCPServer.Contexts.LockList;
  try
    for Count := 0 to cList.Count - 1 do
      TCliContext(cList[Count]).Queue.Add(Message);
  finally
    IdTCPServer.Contexts.UnlockList;
  end;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  IdTCPServer.ContextClass := TCliContext;
end;

procedure TMainForm.IdTCPServerConnect(AContext: TIdContext);
begin
  TCliContext(AContext).Queue.Clear;
  TCliContext(AContext).Heartbeat_time := now;
  TCliContext(AContext).Activity_time := now;
  TIdSync.SynchronizeMethod(TCliContext(AContext).info_about_start_connection);
  // is it safe?
end;

procedure TMainForm.IdTCPServerExecute(AContext: TIdContext);
var
  tmplist, Queue: TStringlist;
  dtNow: TDateTime;
begin
  dtNow := now;
  tmplist := nil;
  try
    Queue := TCliContext(AContext).Queue.Lock;
    try
      if Queue.Count > 0 then
      begin
        tmplist := TStringlist.Create;
        tmplist.Assign(Queue);
        Queue.Clear;
      end;
    finally
      TCliContext(AContext).Queue.Unlock;
    end;
    if tmplist <> nil then
    begin
      AContext.Connection.IOHandler.Write(tmplist);
      TCliContext(AContext).Heartbeat_time := dtNow;
    end;
  finally
    tmplist.Free;
  end;

  if SecondsBetween(dtNow, TCliContext(AContext).Heartbeat_time) > 30 then
  begin
    AContext.Connection.IOHandler.WriteLn('E:');
    TCliContext(AContext).Heartbeat_time := dtNow;
  end;

  if SecondsBetween(dtNow, TCliContext(AContext).Activity_time) > 6 then
  begin
    AContext.Connection.Disconnect;
    Exit;
  end;
  TCliContext(AContext).ProccessMsg;;
end;

procedure TMainForm.Button1Click(Sender: TObject);
begin
  IdTCPServer.Active := true;
end;

procedure TMainForm.Button2Click(Sender: TObject);
begin
  IdTCPServer.Active := false;
  // here application freezes when there are more then tens active clients
end;

procedure TMainForm.Button3Click(Sender: TObject);
begin
  BroadcastMessage('Hello');
  // is it safe and correct?
end;

更新(在您的优秀答案之后的最后一个问题)为了更简单(更短的代码长度),我可以使用如下的TIdNotify类:

TMyNotify.Create(1, 'ABC').Notify; 

type
  TMyNotify = class(TidNotify)
  public
    faction: string;
    fdata:string;
    procedure DoNotify; override;
    procedure action1();
    procedure action2();
    constructor Create(action:integer;fdata:string); reintroduce;
  end;

constructor TMyNotify.Create(action:integer;fdata:string); reintroduce;
begin
  inherited Create;
  faction:=action;
  fdata:=data;
end;

procedure TMyNotify.action2()
begin
  //use fdata and do something with vcl etc.
end;

procedure TMyNotify.action2()
begin
  //use fdata and do something with vcl etc.
end;

procedure TMyNotify.DoNotify;
begin
  case action of
    1: action1()
    2: action2()
  end;
end;

再次感谢你以前的帮助

1 个答案:

答案 0 :(得分:8)

  

当我使用Button3Click程序将BroadCast从GUI发送到连接的客户端时 - 这是正确的方法(我的意思是:它是否安全)?

是的,您正确且安全地发送数据。但是,TCliContext.ProcessMsg()正在对ReadLn()执行阻止调用。如果客户端暂时不发送任何数据,则该逻辑会阻止您的OnExecute代码及时执行其时间敏感逻辑(如果有的话)。由于您涉及时间敏感逻辑,因此您需要在连接处理上使用超时,以便您的时间检查具有运行的机会。在有可读取的实际数据(或ProcessMsg()内部处理超时)之前,请不要调用ProcessMsg(),并且您应该为TIdIOHandler.ReadTimeout属性分配一个值,以便进行更好的衡量客户端停止在消息中间发送数据。例如:

procedure TMainForm.IdTCPServerConnect(AContext: TIdContext);
begin
  ...
  AContext.Connection.IOHandler.ReadTimeout := 10000;
end;

procedure TMainForm.IdTCPServerExecute(AContext: TIdContext);
var
  ...
begin
  ...

  if AContext.Connection.IOHandler.InputBufferIsEmpty then
  begin
    if not AContext.Connection.IOHandler.CheckForDataOnSource(100) then
    begin
      AContext.Connection.IOHandler.CheckForDisconnect;
      Exit;
    end;
  end;

  TCliContext(AContext).ProccessMsg;
  TCliContext(AContext).Activity_time := Now();
end;
  

我可以输入类似于DoSomethingSafe方法的代码,我在其中创建与DB的连接,对其执行某些操作,并关闭与DB的连接?这样安全吗?

是。实际上,特别是对于数据库查询,应尽可能为每个客户端线程提供与数据库的自己的连接。然后,您不必同步数据库查询(根据所使用的数据库,您甚至可以在查询本身中使用数据库提供的同步锁)。如果可能,您还应该汇集数据库连接(由于架构限制,某些数据库类型不可用。例如,ADO由于使用了特定于线程的ActiveX / COM对象)。如果不必要,请不要跨多个线程同步数据库连接。当您需要执行数据库查询时,从池中获取数据库连接(或者根据需要创建新连接),执行数据库查询,然后将数据库连接放回池中(如果可能),以便另一个客户端线程可以在需要时使用它。如果数据库连接在池中一段时间​​,请断开连接,然后在需要再次使用时重新连接。这有助于将数据库连接数量保持在最小,同时最大限度地提高其使用率。

  

为什么我的应用程序冻结时有超过20个clietns并且我想通过使用它的active:= false(button2.click方法)来停止工作服务器?

发生这种情况的最常见原因是您可能正在启动对主线程的同步操作(或者已经处于同步操作的中间),同时在主线程内停用服务器。这是一个有保障的死锁场景。请记住,每个客户端都在服务器内的自己的线程中运行。当主线程停用服务器时,它在等待服务器完成停用时被阻止,因此它无法处理同步请求。服务器停用等待所有客户端线程完全终止。在等待主线程处理同步请求时,同步客户端线程被阻塞,因此无法终止。发生死锁。客户端数量无关紧要,即使只连接了一个客户端也可能发生这种情况。

要解决这个问题,您有几个选择:

  1. 创建一个工作线程来停用服务器,而不是让主线程停用它。这将释放主线程以正常处理同步请求,允许客户端线程正常终止,服务器正常完全停用。例如:

    type
      TShutdownThread = class(TThread)
      protected
        procedure Execute; override;
      end;
    
    procedure TShutdownThread.Execute;
    begin
      MainForm.IdTCPServer.Active := False;
    end;
    
    procedure TMainForm.Button2Click(Sender: TObject);
    begin
      if MainForm.IdTCPServer.Active then
      begin
        with TShutdownThread.Create(False) do
        try
          WaitFor; // internally processes sync requests...
        finally
          Free;
        end;
      end;
    end;
    
  2. 尽可能消除线程阻塞同步。在客户端线程中直接执行尽可能多的工作,而不是在主线程中。特别是对于您的客户端代码实际上不必等待来自主线程的响应的操作。如果实际上不需要跨线程边界同步某些内容,则不要同步它。如果必须与主线程同步,请尽可能使用TIdNotify代替TIdSyncTIdNotify是异步的,因此它不像TIdSync那样阻塞调用线程,从而避免了停用死锁。您只需要对TIdNotify更加小心,因为它是异步的。它被放入后台队列并在以后执行,因此您必须确保使用它访问的任何对象和数据在最终运行时仍然有效。出于这个原因,最好使TIdNotify实现尽可能自包含,这样它们就不依赖于外部事物。例如:

    type
      TMemoNotify = class(TIdNotify)
      protected
        FStr: String;
        procedure DoNotify; override;
      public
        class procedure AddToMemo(const Str: string);
      end;
    
    procedure TMemoNotify.DoNotify;
    begin
      MainForm.Memo1.Lines.Add(FStr);
    end;
    
    class procedure TMemoNotify.AddToMemo(const Str: string);
    begin
      with Create do
      begin
        FStr := Str;
        Notify;
        // DO NOT free it!  It is self-freeing after it is run later on...
      end;
    end;
    
    procedure TCliContext.ProcessMsg;
    var
      Msg: string;
    begin
      Msg := Connection.IOHandler.ReadLn;
      TMemoNotify.AddToMemo(Msg);
      ...
    end;
    
    procedure TMainForm.IdTCPServerConnect(AContext: TIdContext);
    begin
      ...
      TCliContext(AContext).Who := ...;
      TMemoNotify.AddToMemo(TCliContext(AContext).Who + ' connected');
      ...
    end;
    
    procedure TMainForm.IdTCPServerDisconnect(AContext: TIdContext);
    begin
      ...
      TMemoNotify.AddToMemo(TCliContext(AContext).Who + ' disconnected');
      ...
    end;
    
  3.   

    我可以在TCliContext.ProccessMsg中使用TCliContext.BroadcastMessage,只是在没有任何同步的情况下调用它吗?

    是的,因为TIdTCPServer.ContextTIdThreadSafeStringList锁提供了足够的同步(它们都在内部使用TCriticalSection。这同样适用于TCliContext.SendMessageTo()

      

    我可以在OnConnect方法中读取(Connection.IOHandler.ReadLn())(我想读取带有登录数据的行并在DB中检查它然后当它不正确时立即断开连接?

    是。 OnConnect(和OnDisconnect)在OnExecute运行的同一客户端线程上下文中运行。TIdTCPServer检查套接字是否在OnConnect退出后仍然连接,之前然后启动OnExecute循环,以防OnConnect确定断开客户端。

      

    我在某处读过,使用IdSync有时会有危险(如果在使用它时出现任何问题)

    在大多数情况下,TIdSyncTIdNotify只要您正确使用它们就可以安全使用。

    同步的

    TIdSync如果主线程被阻塞,确实会有死锁的可能性。

    如果您使用TIdNotify,请确保您使用的是Indy 10的最新版本。某些早期版本的Indy 10在TIdNotify中有内存泄漏,但最近已修复

      

    什么是更好的解决方案来达到全局变量或VCL对象?

    与任何给定线程没有严格关联的全局变量应尽可能提供自己的同步。无论是在他们自己的内部代码中(如BroadcastMessage()SendMessageTo()实现),还是通过单独的锁,如TCriticalSection个对象。

    VCL对象只能在主线程中访问,所以如果你不使用TIdSync / TIdNotify,你必须使用你选择的其他形式的线程同步来委托你的代码在主线程的上下文中运行。这就是UI逻辑和业务逻辑的分离真正发挥作用的地方。如果可能,您应该将业务数据与UI分开,然后围绕数据操作提供安全的线程间锁定,然后您可以让UI在需要时安全地更新数据,并让工作线程在需要时安全地更新数据。向UI发布异步请求以显示最新数据。