2

Remy Lebeauによる専門的な質問とほぼ満足のいく回答によると(ありがとうございます)、アプリケーションに役立つコードを組み合わせようとしています。そして、いくつかの側面が私に不明確なままです。以下のコードを見ると:

  • Button3Clickプロシージャを使用してGUIから接続されたクライアントにBroadCastを送信する場合-それは正しい方法ですか(つまり、安全ですか)?
  • DBへの接続を作成し、それに何かを実行して、DBへの接続を閉じるDoSomethingSafeメソッドコードと同様のものを入れることはできますか?安全ですか?
  • clietnsが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から接続されたクライアントにBroadCastを送信する場合-それは正しい方法ですか(つまり、安全ですか)?

はい、データを正しく安全に送信しています。ただし、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;

DBへの接続を作成し、それに何かを実行して、DBへの接続を閉じるDoSomethingSafeメソッドコードと同様のものを入れることはできますか?安全ですか?

はい。実際、特にDBクエリの場合、可能な限り、各クライアントスレッドにDBへの独自の接続を与える必要があります。そうすれば、DBクエリを同期する必要はありません(使用するDBによっては、クエリ自体でDBが提供する同期ロックを使用できる場合もあります)。可能であれば、DB接続もプールする必要があります(一部のDBタイプは、アーキテクチャ上の制限のためにプールできません。たとえば、スレッド固有のActiveX / COMオブジェクトを使用するためにADOを使用します)。必要がない場合は、複数のスレッド間でDB接続を同期しないでください。DBクエリを実行する必要がある場合は、プールからDB接続を取得し(または必要に応じて新しい接続を作成し)、DBクエリを実行してから、DB接続をプールに戻し(可能な場合)、別のクライアントスレッドができるようにします。必要に応じて使用してください。DB接続がしばらくの間プールにある場合、切断してから、再度使用する必要がある場合は再接続してください。これにより、DB接続の数を最小限に抑えながら、使用量を最大化できます。

clietnsが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.ContextTIdThreadSafeStringListロックが適切な同期を提供しているためです(どちらもTCriticalSection内部で使用されます)。同じことが同様に当てはまりTCliContext.SendMessageTo()ます。

OnConnectメソッドで(Connection.IOHandler.ReadLn())を読み取ることはできますか(ログインデータを含む行を読み取り、DBで確認したいのですが、正しくない場合はすぐに切断しますか?

はい。 OnConnect(およびOnDisconnect)は、で実行されるのと同じクライアントスレッドコンテキストでOnExecute実行され ます。クライアントを切断することを決定した場合に備えて、終了TIdTCPServer後もソケットが接続されているかどうかを確認し、ループOnConnectを開始します。OnExecuteOnConnect

私はどこかで読んだ、IdSyncを使用することは時々危険です(それを使用して何かがうまくいかない場合)

ほとんどの場合、TIdSync正しくTIdNotify使用する限り安全に使用できます。

TIdSync、同期しているため、メインスレッドがブロックされた場合、デッドロックが発生する可能性があります。それだけです。

を使用する場合はTIdNotify、最新バージョンのIndy 10を使用していることを確認してください。以前のリリースのIndy10の一部でメモリリークが発生していましたTIdNotifyが、最近修正されました。

グローバル変数またはVCLオブジェクトに到達するためのより良い解決策は何ですか?

特定のスレッドに厳密に関連付けられていないグローバルは、可能な場合は独自の同期を提供する必要があります。独自の内部コード(BroadcastMessage()および実装など)であるか、オブジェクトSendMessageTo()などの個別のロックを介しているか。TCriticalSection

TIdSyncVCLオブジェクトはメインスレッド内でのみアクセスする必要があるため、 /を使用しない場合TIdNotifyは、メインスレッドのコンテキストで実行するようにコードを委任するために、選択した他の形式のスレッド同期を使用する必要があります。ここで、UIロジックとビジネスロジックの分離が実際に機能します。可能であれば、ビジネスデータをUIから分離し、データ操作の周りに安全なスレッド間ロックを提供する必要があります。そうすれば、UIに必要なときにデータを安全に更新させ、ワーカースレッドに必要なときにデータを安全に更新させることができます。 UIに非同期リクエストを送信して、最新のデータを表示します。

于 2013-01-06T07:51:28.250 に答える