根据Remy Lebeau的几个问题和几乎令人满意的答案(再次感谢你)我尝试将代码组合起来用于我的应用程序。 有几个方面让我不清楚。当你看下面的代码时:
我的示例代码如下:
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;
再次感谢你以前的帮助
答案 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方法)来停止工作服务器?
发生这种情况的最常见原因是您可能正在启动对主线程的同步操作(或者已经处于同步操作的中间),同时在主线程内停用服务器。这是一个有保障的死锁场景。请记住,每个客户端都在服务器内的自己的线程中运行。当主线程停用服务器时,它在等待服务器完成停用时被阻止,因此它无法处理同步请求。服务器停用等待所有客户端线程完全终止。在等待主线程处理同步请求时,同步客户端线程被阻塞,因此无法终止。发生死锁。客户端数量无关紧要,即使只连接了一个客户端也可能发生这种情况。
要解决这个问题,您有几个选择:
创建一个工作线程来停用服务器,而不是让主线程停用它。这将释放主线程以正常处理同步请求,允许客户端线程正常终止,服务器正常完全停用。例如:
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;
尽可能消除线程阻塞同步。在客户端线程中直接执行尽可能多的工作,而不是在主线程中。特别是对于您的客户端代码实际上不必等待来自主线程的响应的操作。如果实际上不需要跨线程边界同步某些内容,则不要同步它。如果必须与主线程同步,请尽可能使用TIdNotify
代替TIdSync
。 TIdNotify
是异步的,因此它不像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.Context
和TIdThreadSafeStringList
锁提供了足够的同步(它们都在内部使用TCriticalSection
。这同样适用于TCliContext.SendMessageTo()
。
我可以在OnConnect方法中读取(Connection.IOHandler.ReadLn())(我想读取带有登录数据的行并在DB中检查它然后当它不正确时立即断开连接?
是。 OnConnect
(和OnDisconnect
)在OnExecute
运行的同一客户端线程上下文中运行。TIdTCPServer
检查套接字是否在OnConnect
退出后仍然连接,之前然后启动OnExecute
循环,以防OnConnect
确定断开客户端。
我在某处读过,使用IdSync有时会有危险(如果在使用它时出现任何问题)
在大多数情况下,TIdSync
和TIdNotify
只要您正确使用它们就可以安全使用。
TIdSync
如果主线程被阻塞,确实会有死锁的可能性。
如果您使用TIdNotify
,请确保您使用的是Indy 10的最新版本。某些早期版本的Indy 10在TIdNotify
中有内存泄漏,但最近已修复
什么是更好的解决方案来达到全局变量或VCL对象?
与任何给定线程没有严格关联的全局变量应尽可能提供自己的同步。无论是在他们自己的内部代码中(如BroadcastMessage()
和SendMessageTo()
实现),还是通过单独的锁,如TCriticalSection
个对象。
VCL对象只能在主线程中访问,所以如果你不使用TIdSync
/ TIdNotify
,你必须使用你选择的其他形式的线程同步来委托你的代码在主线程的上下文中运行。这就是UI逻辑和业务逻辑的分离真正发挥作用的地方。如果可能,您应该将业务数据与UI分开,然后围绕数据操作提供安全的线程间锁定,然后您可以让UI在需要时安全地更新数据,并让工作线程在需要时安全地更新数据。向UI发布异步请求以显示最新数据。