【问题标题】:TIdServer, about synchronization againTIdServer,关于再次同步
【发布时间】:2013-01-07 04:40:24
【问题描述】:

根据 Remy Lebeau 的几个问题和几乎满足的回答(再次感谢您),我尝试结合对我的应用程序有用的代码。 有几个方面让我不清楚。当您查看以下代码时:

  • 当我使用 Button3Click 程序将广播从 GUI 发送到连接的客户端时 - 方法是否正确(我的意思是:它安全吗?
  • 我可以放入类似于 DoSomethingSafe 的方法代码,在其中创建与 DB 的连接、在其上执行某些操作并关闭与 DB 的连接吗?安全吗?
  • 为什么我的应用程序在超过 20 个客户端时冻结,我想通过使用其 active:=false(button2.click 方法)来停止工作服务器?
  • 我可以在 TCliContext.ProccessMsg 中使用 TCliContext.BroadcastMessage 来调用它而不进行任何同步吗?
  • 我可以在 OnConnect 方法中读取 (Connection.IOHandler.ReadLn()) 吗(我想读取登录数据行并在数据库中检查它,然后当它不正确时立即断开连接?
  • 我在某处读到,使用 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;

再次感谢您之前的帮助

【问题讨论】:

    标签: synchronization indy


    【解决方案1】:

    当我使用 Button3Click 程序将广播从 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 的连接吗?安全吗?

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

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

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

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

    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;
      

    我可以在 TCliContext.ProccessMsg 中使用 TCliContext.BroadcastMessage 来调用它而不进行任何同步吗?

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

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

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

    我在某处读到,使用 IdSync 有时是有风险的(如果使用它内部出现任何问题)

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

    TIdSync 是同步的,如果主线程被阻塞,确实有死锁的可能性,仅此而已。

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

    访问全局变量或 VCL 对象的更好解决方案是什么?

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

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

    【讨论】:

    • 优秀和完整的答案。我很感激。但是,如果可以,请回答是否使用在我的主要问题末尾添加的 TidNotify 示例是否可以?
    • 这是我见过的关于使用 Indy 实现 tcp 服务器的最佳帖子之一。它总结了我在过去 4 年中学到的东西。 @RemyLebeau 谢谢。
    猜你喜欢
    • 2021-11-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-08
    • 1970-01-01
    • 1970-01-01
    • 2018-01-21
    • 1970-01-01
    相关资源
    最近更新 更多