2

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

  • 当我使用 Button3Click 程序将广播从 GUI 发送到连接的客户端时 - 这是正确的方法(我的意思是:它安全吗)?
  • 我可以放入类似于 DoSomethingSafe 的方法代码,在其中创建与 DB 的连接、在其上执行某些操作并关闭与 DB 的连接吗?安全吗?
  • 为什么我的应用程序在超过 20 个客户端时冻结,并且我想通过使用它的 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;

再次感谢您之前的帮助

4

1 回答 1

8

当我使用 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而不是。是异步的,所以它不会像这样阻塞调用线程,从而避免了去激活死锁。你只需要小心一点TIdSyncTIdNotifyTIdSyncTIdNotify,因为它是异步的。它被放入后台队列并在稍后执行,因此您必须确保您使用它访问的任何对象和数据在它最终运行时仍然有效。出于这个原因,最好使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.ContextandTIdThreadSafeStringList锁提供了足够的同步(它们都在TCriticalSection内部使用)。这同样适用于TCliContext.SendMessageTo()

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

是的。 OnConnect(and 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 发布异步请求以显示最新数据。

于 2013-01-06T07:51:28.250 回答