indy 10终于随着Delphi2005发布了,不过indy套件在我的印象中总是复杂并且BUG不断,说实话,不是看在他一整套组件的面子上,我还是喜欢VCL原生的Socket组件,简洁,清晰。Indy9发展到了indy10几乎完全不兼容,可叹啊。言归正传。在使用IdTCPServer组件的时候发现了他的漏洞,他的OnConnec,OnExecute,OnDisconnect等事件是在其他线程中执行的,通常情况下这没有问题,但是在特殊的情况下会造成问题,如果其他部分的程序写得有问题就会出现漏洞。
我发现的漏洞是这样的,我在OnDisconnect事件中释放一个ListView的一个对应的Item,也就是,一个客户端离开的时候,界面上的ListView对应的项目删除。正常情况没有任何问题,但是,如果不断开连接直接关闭程序就会死掉,事实上我在程序中Form的CloseQuery事件中作了处理,在这个事件中我关闭连接,但是,没有效果,只有在程序中手动的点鼠标关闭才不会死掉,问题出在哪里?
问题出在这里,ListView是一个Windows的标准组件,释放他的一个Item是通过消息完成的,也就是说,我在OnDisconnect中在一个非主线程的线程中向主线程的窗口发送消息并且等待返回。而此时主线程在干嘛呢?因为是主线程触发的DisConnect,所以他在等待这个端口的服务线程挂起,这样就发生死锁,问题在于,主线程的等待服务线程挂起的处理代码不当,它理论上应该在等待同时处理消息。原代码如下所示
procedure TIdSchedulerOfThread.TerminateYarn(AYarn: TIdYarn);
var
LYarn: TIdYarnOfThread;
begin
LYarn := TIdYarnOfThread(AYarn);
if LYarn.Thread.Suspended then begin //判断是否挂起了,挂起了才释放线程
// If suspended, was created but never started
// ie waiting on connection accept
LYarn.Thread.Free;
FreeAndNil(LYarn);
end else begin
// Is already running and will free itself
LYarn.Thread.Stop; //没完没了的调用Stop过程,却不处理任何消息和同步事件
// Dont free the yarn. The thread frees it (IdThread.pas)
end;
end;
它的上一级调用者,没完没了的判断服务线程数量,然后没完没了地调用上面这个函数,调用者原代码如下
procedure TIdTCPServer.TerminateAllThreads;
var
i: Integer;
begin
// TODO: reimplement support for TerminateWaitTimeout
//BGO: find out why TerminateAllThreads is sometimes called multiple times
//Kudzu: Its because of notifications. It calls shutdown when the Scheduler is
// set to nil and then again on destroy.
if Contexts <> nil then begin
with Contexts.LockList do try
for i := 0 to Count - 1 do begin
// Dont call disconnect with true. Otheriwse it frees the IOHandler and the thread
// is still running which often causes AVs and other.
TIdContext(Items[i]).Connection.Disconnect(False);
end;
finally Contexts.UnLockList; end;
end;
// Scheduler may be nil during destroy which calls TerminateAllThreads
// This happens with explicit schedulers
if Scheduler <> nil then begin
Scheduler.TerminateAllYarns;
end;
end;
说实话,我很不理解indy线程对象又是stop又是start的复杂模型意义何在,而且非常容易出问题,简单的线程模型更加可靠和实用。
修改的方法很简单,但是考虑到兼容Linux和其他平台的问题,还必须进行隔离分解层次,所以稍微复杂了一点点。就是在idThread类中增加一个公开的方法ProcessMessages,然后在TerminateYarn中调用。代码如下
procedure TIdThread.ProcessMessages;
begin
{$IFDEF MSWINDOWS}
if GetCurrentThreadID = MainThreadID then
begin
CheckSynchronize;
Application.ProcessMessages;
end;
{$ENDIF}
{$IFDEF LINUX}
if GetCurrentThreadID = MainThreadID then
begin
CheckSynchronize(1000);
end;
{$ENDIF}
end;