Search code examples
multithreadingdelphiindy

multi-thread causes memory leak


I'm made an application that receives data from an API every 10 seconds and this is all done within 2 threads. I can't figure out why there is a memory leak as I free all that I used after the task is done.

I want to apologize if I'm missing or doing something wrong as I'm new to threading and I don't fully understand how it works.

I tried to remake this: Synchronizing Threads and GUI in a Delphi Application but failed as I don't understand what is going on. If someone can please explain to me what I'm doing wrong and how I can fix it or make it better.

Current Code:

unit uMain;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls,
  IdBaseComponent, IdComponent, IdTCPConnection, IdTCPClient, IdHTTP,
  IdSSLOpenSSL, IdIOHandler, IdIOHandlerSocket, IdIOHandlerStack, IdSSL;

type
  TfrmMain = class(TForm)
    grpLuno: TGroupBox;
    lblBid: TLabel;
    lblRolling24HourVolume: TLabel;
    grpBinance: TGroupBox;
    tmrRefresh: TTimer;
    lblPrice: TLabel;
    lblBinanceVolume: TLabel;
    lbl1: TLabel;
    lbl24hChange: TLabel;
    procedure Refresh;
    procedure tmrRefreshTimer(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

type
  TGetLuno = class(TThread)
  protected
    procedure Execute; override;
  end;

type
  TGetBinance = class(TThread)
  protected
    procedure Execute; override;
  end;

var
  frmMain: TfrmMain;

implementation

{$R *.dfm}

uses
  djson, DateUtils, Math;

{ TForm1 }

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NoMove or SWP_NoSize);
  Refresh;
end;

procedure TfrmMain.Refresh;
begin
  with TGetLuno.Create do
  begin
    FreeOnTerminate := True;
  end;

  with TGetBinance.Create do
  begin
    FreeOnTerminate := True;
  end;

end;

procedure TfrmMain.tmrRefreshTimer(Sender: TObject);
begin
  Refresh;
end;

{ TGetLuno }

procedure TGetLuno.Execute;
var
  httpclient: TIdHTTP;
  sdata: string;
  jdata: TJSON;
begin
  httpclient := TIdHTTP.Create(nil);
  try
    sdata := httpclient.Get('https://api.mybitx.com/api/1/ticker?pair=XBTZAR');
  finally
    httpclient.Free;
  end;

  jdata := TJSON.Parse(sdata);
  try
    frmMain.lblBid.Caption := 'Price: R ' + jdata['bid'].AsString;
    frmMain.lblRolling24HourVolume.Caption := 'Volume: ' + jdata['rolling_24_hour_volume'].AsString;
  finally
    jdata.Free;
  end;
end;

{ TGetBinance }

procedure TGetBinance.Execute;
var
  httpclient: TIdHTTP;
  sdata: string;
  jdata: TJSON;
  SocketOpenSSL: TIdSSLIOHandlerSocketOpenSSL;
begin
  httpclient := TIdHTTP.Create(nil);
  SocketOpenSSL := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
  SocketOpenSSL.SSLOptions.SSLVersions := [sslvTLSv1, sslvTLSv1_1, sslvTLSv1_2];
  httpclient.IOHandler := SocketOpenSSL;
  try
    sdata := httpclient.Get('https://api.binance.com/api/v1/ticker/24hr?symbol=BTCUSDT');
  finally
    httpclient.Free;
  end;

  jdata := TJSON.Parse(sdata);
  try
    frmMain.lblPrice.Caption := 'Price: $ ' + jdata['lastPrice'].AsString;
    frmMain.lblBinanceVolume.Caption := 'Volume: ' + jdata['volume'].AsString;

    if StrToFloat(StringReplace(jdata['priceChangePercent'].AsString, '.', ',', [rfReplaceAll, rfIgnoreCase])) > 0 then
      frmMain.lbl24hChange.Font.Color := clLime
    else
      frmMain.lbl24hChange.Font.Color := clRed;
    frmMain.lbl24hChange.Caption := StringReplace(FloatToStr(RoundTo(StrToFloat(StringReplace(jdata['priceChangePercent'].AsString, '.', ',', [rfReplaceAll, rfIgnoreCase])), -2)), ',', '.', [rfReplaceAll, rfIgnoreCase]) + ' %';
  finally
    jdata.Free;
  end;
end;

initialization
  ReportMemoryLeaksOnShutdown := True;

end.

Solution

  • Inside your thread execute methods, you are accessing VCL frmMain components directly.

    Since the VCL framework must be executed in the main thread only, you will need to divert those calls. Use TThread.Synchronize() or TThread.Queue() for example:

    jdata := TJSON.Parse(sdata);
    try
      Synchronize(
        procedure
        begin
          frmMain.lblPrice.Caption := 'Price: $ ' + jdata['lastPrice'].AsString;
          frmMain.lblBinanceVolume.Caption := 'Volume: ' + jdata['volume'].AsString;
    
          if StrToFloat(StringReplace(jdata['priceChangePercent'].AsString, '.',
            ',', [rfReplaceAll, rfIgnoreCase])) > 0 then
            frmMain.lbl24hChange.Font.Color := clLime
          else
            frmMain.lbl24hChange.Font.Color := clRed;
          frmMain.lbl24hChange.Caption :=
            StringReplace(FloatToStr(RoundTo(StrToFloat(StringReplace(jdata
            ['priceChangePercent'].AsString, '.', ',', [rfReplaceAll, rfIgnoreCase])
            ), -2)), ',', '.', [rfReplaceAll, rfIgnoreCase]) + ' %';
        end);
    finally
      jdata.Free;
    end;