You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

修复Delphi线程中TIdHTTP访问违规及线程安全问题问询

线程终止后TIdHTTP持续执行导致访问违规的解决方案

问题描述

线程终止后,TIdHTTP仍继续执行阻塞的Get请求(带30秒超时的长轮询),无法及时响应Terminated标记,最终引发访问违规(Access violation)错误。已尝试标记退出循环、手动断开组件等方法,均未解决问题,需调整代码实现线程安全并修复异常。


现有代码片段

线程构造函数与析构函数

constructor TTelegramListener.Create(Asyspended: Boolean);
begin
  FFlag := False;
  FreeOnTerminate := True;
  inherited Create(Asyspended);
end;

destructor TTelegramListener.Destroy;
begin
  FCallback := nil;
  inherited;
end;

线程调用与创建逻辑

procedure TTeleBot.StartListenMessages(CallProc: TCallbackProc);
begin
  if Assigned(FMessageListener) then
    FMessageListener.DoTerminate;
  FMessageListener := TTelegramListener.Create(False);
  FMessageListener.Priority := tpLowest;
  FMessageListener.FreeOnTerminate := True;
  FMessageListener.Callback :=  CallProc;
  FMessageListener.TelegramToken := FTelegramToken;
end;

线程终止代码

if Assigned(FMessageListener) then
  FMessageListener.Terminate;

线程执行逻辑

procedure TTelegramListener.Execute;
var
  LidHTTP: TIdHTTP;
  LSSLSocketHandler: TIdSSLIOHandlerSocketOpenSSL;
  Offset, PrevOffset: Integer;
  LJSONParser: TJSONObject;
  LResronseList: TStringList;
  LArrJSON: TJSONArray;
begin
  Offset := 0;
  PrevOffset := 0;
  //create a local indy http component
  try
    LidHTTP := TIdHTTP.Create;
    LidHTTP.HTTPOptions := LidHTTP.HTTPOptions + [hoNoProtocolErrorException];
    LidHTTP.Request.BasicAuthentication := False;
    LidHTTP.Request.CharSet := 'utf-8';
    LidHTTP.Request.UserAgent := 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0';

    LSSLSocketHandler := TIdSSLIOHandlerSocketOpenSSL.Create(LidHTTP);
    LSSLSocketHandler.SSLOptions.Method := sslvTLSv1_2;
    LSSLSocketHandler.SSLOptions.SSLVersions := [sslvTLSv1_2];
    LSSLSocketHandler.SSLOptions.Mode := sslmUnassigned;
    LSSLSocketHandler.SSLOptions.VerifyMode := [];
    LSSLSocketHandler.SSLOptions.VerifyDepth := 0;

    LidHTTP.IOHandler := LSSLSocketHandler;

    LJSONParser := TJSONObject.Create;
    LResronseList := TStringList.Create;
  except
   on E: Exception do
   begin
    FLastError := 'Error of create objects';
    FreeAndNil(LidHTTP);
    FreeAndNil(LJSONParser);
    FreeAndNil(LResronseList);
   end;
  end;
  try
    while not Terminated do
    begin

      LJSONParser := TJSONObject.Create;
      if Assigned(LidHTTP) then
      begin
        FResponse := LidHTTP.Get(cBaseUrl + FTelegramToken + '/getUpdates?offset=' + IntToStr(Offset) + '&timeout=30');
        if FResponse.Trim = '' then
          Continue;
        LArrJSON := ((TJSONObject.ParseJSONValue(FResponse) as TJSONObject).GetValue('result') as TJSONArray);

        if lArrJSON.Count <= 0 then Continue;

        LResronseList.Clear;
        for var I := 0 to LArrJSON.Count - 1 do
          LResronseList.Add(LArrJSON.Items[I].ToJSON);

        Offset := LResronseList.Count;
        if Offset > PrevOffset then
        begin
          LJSONParser := TJSONObject.ParseJSONValue(LResronseList[LResronseList.Count - 1], False, True) as TJSONObject;
          if (LJSONParser.FindValue('message.text') <> nil) and (LJSONParser.FindValue('message.text').Value.Trim <> '') then
          begin
            if LJSONParser.FindValue('message.from.id') <> nil then
              FUserID := LJSONParser.FindValue('message.from.id').Value; //Его ИД по которому можем ему написать

            if LJSONParser.FindValue('message.from.first_name') <> nil then
              FUserName := LJSONParser.FindValue('message.from.first_name').Value;

            if (LJSONParser.FindValue('message.from.first_name') <> nil) and (LJSONParser.FindValue('message.from.last_name') <> nil) then
              FUserName := LJSONParser.FindValue('message.from.first_name').Value + ' ' + LJSONParser.FindValue('message.from.last_name').Value; //Это имя написавшего боту

            if LJSONParser.FindValue('message.text') <> nil then
              FUserMessage :=  LJSONParser.FindValue('message.text').Value;  //Текст сообщения
            Synchronize(Status); // Сообщим что есть ответ
          end;

          if LJSONParser <> nil then
            LJSONParser.Free;
          PrevOffset := LResronseList.Count;
        end;
      end;
    end;
  finally
    FreeAndNil(LidHTTP);
    FreeAndNil(LJSONParser);
    FreeAndNil(LResronseList);
  end;
end;

Status回调逻辑

procedure TTelegramListener.Status;
begin
  if Assigned(FCallback) then
    FCallback(FUserID, FUserName, FUserMessage);
end;

修复方案与代码调整

1. 让线程终止时主动中断TIdHTTP请求

TIdHTTP的Get方法是阻塞的(尤其是30秒超时的长轮询),仅调用Terminate无法让线程跳出循环。需要重写Terminate方法,主动断开HTTP连接:

procedure TTelegramListener.Terminate;
begin
  inherited Terminate;
  // 中断阻塞的HTTP请求,让Get方法立即返回
  if Assigned(LidHTTP) then
    LidHTTP.Disconnect;
end;

2. 修正线程启动与终止的安全逻辑

原代码中直接替换线程对象会导致旧线程未完全销毁就被覆盖,引发野指针。需等待旧线程终止后再创建新线程:

procedure TTeleBot.StartListenMessages(CallProc: TCallbackProc);
var
  OldListener: TTelegramListener;
begin
  OldListener := FMessageListener;
  if Assigned(OldListener) then
  begin
    OldListener.Terminate;
    // 等待旧线程完全终止(因FreeOnTerminate=True,避免野指针)
    while OldListener.Terminated and not OldListener.Finished do
      Sleep(10);
    FMessageListener := nil;
  end;

  FMessageListener := TTelegramListener.Create(False);
  FMessageListener.Priority := tpLowest;
  FMessageListener.FreeOnTerminate := True;
  FMessageListener.Callback := CallProc;
  FMessageListener.TelegramToken := FTelegramToken;
end;

3. 修复Execute方法的内存泄漏与线程安全问题

原代码存在重复创建对象、offset计算错误、同步时竞态条件等问题,调整后的Execute方法:

procedure TTelegramListener.Execute;
var
  LidHTTP: TIdHTTP;
  LSSLSocketHandler: TIdSSLIOHandlerSocketOpenSSL;
  Offset, PrevOffset: Integer;
  LJSONParser: TJSONObject;
  LResponseList: TStringList;
  LArrJSON: TJSONArray;
  LResponse: string;
begin
  Offset := 0;
  PrevOffset := 0;
  LidHTTP := nil;
  LJSONParser := nil;
  LResponseList := nil;

  try
    // 资源仅初始化一次,避免循环内重复创建
    LidHTTP := TIdHTTP.Create(nil);
    LidHTTP.HTTPOptions := LidHTTP.HTTPOptions + [hoNoProtocolErrorException];
    LidHTTP.Request.BasicAuthentication := False;
    LidHTTP.Request.CharSet := 'utf-8';
    LidHTTP.Request.UserAgent := 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0';

    LSSLSocketHandler := TIdSSLIOHandlerSocketOpenSSL.Create(LidHTTP);
    LSSLSocketHandler.SSLOptions.Method := sslvTLSv1_2;
    LSSLSocketHandler.SSLOptions.SSLVersions := [sslvTLSv1_2];
    LSSLSocketHandler.SSLOptions.Mode := sslmUnassigned;
    LSSLSocketHandler.SSLOptions.VerifyMode := [];
    LSSLSocketHandler.SSLOptions.VerifyDepth := 0;

    LidHTTP.IOHandler := LSSLSocketHandler;
    LResponseList := TStringList.Create;

    while not Terminated do
    begin
      if not Assigned(LidHTTP) then Break;

      try
        // 请求前先检查终止标记,避免进入阻塞请求
        if Terminated then Break;
        LResponse := LidHTTP.Get(cBaseUrl + FTelegramToken + '/getUpdates?offset=' + IntToStr(Offset) + '&timeout=30');
      except
        on E: Exception do
        begin
          // 捕获Disconnect引发的中断异常,直接退出循环
          if Terminated then Break;
          FLastError := 'HTTP请求错误: ' + E.Message;
          Sleep(1000); // 出错后延迟重试
          Continue;
        end;
      end;

      if Terminated then Break;
      if LResponse.Trim = '' then
      begin
        Sleep(100);
        Continue;
      end;

      try
        LJSONParser := TJSONObject.ParseJSONValue(LResponse) as TJSONObject;
        if not Assigned(LJSONParser) then Continue;

        LArrJSON := LJSONParser.GetValue('result') as TJSONArray;
        if not Assigned(LArrJSON) or (LArrJSON.Count <= 0) then Continue;

        LResponseList.Clear;
        for var I := 0 to LArrJSON.Count - 1 do
          LResponseList.Add(LArrJSON.Items[I].ToJSON);

        // 修正offset计算:Telegram要求用最后一个update_id+1来获取新消息
        Offset := (LArrJSON.Items[LArrJSON.Count-1] as TJSONObject).GetValue<Integer>('update_id') + 1;
        if Offset > PrevOffset then
        begin
          var LastMsgJSON := TJSONObject.ParseJSONValue(LResponseList[LResponseList.Count-1]) as TJSONObject;
          try
            if Assigned(LastMsgJSON) then
            begin
              var MsgText := LastMsgJSON.GetValue<string>('message.text', '');
              if MsgText.Trim <> '' then
              begin
                // 用临时变量存储数据,避免同步时成员变量被修改引发竞态
                var UserID := LastMsgJSON.GetValue<string>('message.from.id', '');
                var FirstName := LastMsgJSON.GetValue<string>('message.from.first_name', '');
                var LastName := LastMsgJSON.GetValue<string>('message.from.last_name', '');
                var UserName := FirstName;
                if LastName <> '' then UserName := UserName + ' ' + LastName;

                // 直接在Synchronize中使用匿名方法,避免依赖线程成员变量
                Synchronize(procedure
                begin
                  if Assigned(FCallback) then
                    FCallback(UserID, UserName, MsgText);
                end);
              end;
            end;
          finally
            LastMsgJSON.Free;
          end;
          PrevOffset := Offset;
        end;
      finally
        LJSONParser.Free; // 确保每次Parse后释放对象,避免内存泄漏
      end;
    end;
  finally
    // 按顺序释放资源,LidHTTP会自动释放其拥有的LSSLSocketHandler
    FreeAndNil(LResponseList);
    FreeAndNil(LidHTTP);
  end;
end;

4. 移除冗余的Status方法

调整后无需单独的Status方法,直接在Synchronize中调用回调,减少中间环节的线程安全风险。


修复说明

  • 中断HTTP请求:解决了阻塞请求无法响应线程终止的核心问题,让线程能及时跳出循环。
  • 线程生命周期管理:等待旧线程完全销毁后再创建新线程,避免野指针和资源冲突。
  • 内存泄漏修复:修正了重复创建JSON对象未释放的问题,确保资源正确回收。
  • 线程安全同步:使用临时变量存储用户数据,避免主线程与工作线程同时访问成员变量引发的访问违规。
  • 业务逻辑修正:修复了Telegram offset的计算逻辑,确保正确获取新消息。

内容的提问来源于stack exchange,提问作者Yaroslav

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 18:05:47