LoginSignup
3
3

More than 5 years have passed since last update.

THttpClient の落とし穴2

Posted at

これは Delphi Advent Calendar 補欠の記事です。

THttpClient とはなんぞや

これについては、落とし穴1をご覧下さい!

処理中アニメーション

THttpClient / TNetHttpClient を使う時 OnReceivedData で、プログレスバーを出したり、くるくる回るアニメーション出したいですよね!?
そこで、前回は、THttpClient を別スレッドで使い、メインスレッドでは、プログレスバーを更新しました。
メインスレッドが動いていないと、くるくる回したりできませんからね!

Android の場合はメインスレッドが5~10秒停止すると、ANR(Application Not Response)で落とされちゃいますしね!

TNetHttpClient の実装

僕が、かたくなに TNetHttpClient を使わないのは TNetHttpClient が残念実装だからです。
具体的なコードをちょっと見てみましょう。

function TNetHTTPClient.Get(const AURL: string; const AResponseContent: TStream;
  const AHeaders: TNetHeaders): IHTTPResponse;
begin
  try
    Result := FHttpClient.Get(AURL, AResponseContent, AHeaders);
    DoOnRequestCompleted(Self, Result);
  except
    on E: Exception do
      DoOnRequestError(Self, E.Message);
  end;
end;

_人人人人人人人人人人人人人_
> まさかのメインスレッド <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

THttpClient.Get はサーバの応答を待って、処理が返ってきます。
落とし穴1にも書きましたがブロッキング動作です。
この実装では、Get を呼んだ段階で、メインスレッドが停止します/(^o^)\
当然、ぐるぐるアニメーションも停止します!!

多分 TNetHttpClient の言い分としては、OnReceivedData 内で、Application.ProcessMessages を呼んだりしてくれよ、ということだと思いますが、そんなん知るか!!

ということで、TNetHttpClient は使い物になりません。
実際、HttpDownload というエンバカデロが提供しているサンプルコードでは、TNetHttpClient を使ってません!
僕と同じように TThread を使って処理を回しています。

まあ、それは置いておいて、今回は OnReceiveData の処理についてです。

OnReceiveData で Synchronize しない!

再び、TNetHttpClient の実装をちょっと見てみます。

procedure TNetHTTPClient.DoOnReceiveData(const Sender: TObject; AContentLength, AReadCount: Int64; var Abort: Boolean);
var
  LAbort: Boolean;
begin
  if Assigned(FOnReceiveData) then
  begin
    LAbort := Abort;
    TThread.Synchronize(nil, procedure
    begin
      FOnReceiveData(Sender, AContentLength, AReadCount, LAbort);
    end);
    Abort := LAbort;
  end;
end;

はい。見事に Synchronize で OnReceiveData を呼び出してますよね。
この実装は一見正しいのです。
というのは、UI の描画はメインスレッドで行う必要があり、THttpClinet.OnReceivedData は別スレッドのままやってくるからです。
なので、Synchronize でメインスレッドと同期して、メインスレッドで UI を描画する訳です。

ですが!
メインスレッドと同期して、UI を描画しているあいだ、ダウンロード処理はどうなってるでしょうか?

もちろん、停止しています!!

さて、ここが問題です。
UI 描画程度で接続が切断されることはないと思いますが、大きなデータをダウンロードしている時に、一々 Synchronize していると、待機時間が馬鹿にならないレベルになります。

どのくらいのオーバーヘッドが生じるのかテストしました。
Android で45[MB]のデータをダウンロードする秒数を3回計測し平均しました。

手法 ダウンロード秒数 備考
Synchronize 96.6
50[msec] 毎にSynchronize 6.3 結局停止してる
CreateAnonymousThread 10.4
TThread を事前に作って使い回す1 6.1
TThread を事前に作って使い回す2 5.0 50[msec]制限つき

※OnReceivedData 毎に CreateAnonymousThread でスレッドを作って、その中で Synchronize

procedure TDownloadThread.HttpReceiveData(
  const Sender: TObject;
  ContentLength, ReadCount: Int64;
  var Abort: Boolean);
begin
  TThread.CreateAnonymousThread(
    procedure
    begin
      TThread.Synchronize(
        TThread.Current,
        procedure
        begin
          // UI 更新処理
        end
      )
    end
  ).Start;
end;

その差、実に90秒以上!!!
Synchronize をすることで、90秒もロスするわけです。

では、ということで 50[msec]毎に Synchronize するようにしてみると、確かに劇的に速くはなりました!ですが、停止していることには変わりありません。

そこで、CreateAnonymousThread にしてみましたが、スレッドの生成にオーバーヘッドがあって、50[msec]毎に Synchronize するバージョンよりも遅い!
しかも、スレッドガンガン立って気分が悪い!

ということで、最適な方法は、事前に Synchronize 専用スレッドを作って、そこから呼ぶ、という方法です。
しかも、一回の描画間隔を 50[msec]以上後、とすると更に良い結果を得られました。

その Synchronize 専用スレッドのソースが↓こちらです。

uDownloadThread.pas(抜粋)
TProgressThread = class(TThread)
private
  [Weak] FDownloadThread: TDownloadThread;
  FRunning: Boolean;
  FAbort: Boolean;
  FReadCount: Int64;
  FContentLength: Int64;
  FStart: TDateTime;
  FProgressTime: Integer;
  FSynchronizer: TMultiReadExclusiveWriteSynchronizer;
protected
  procedure Execute; override;
public
  constructor Create(
    const iDownloadThread: TDownloadThread;
    const iProgressTime: Integer); reintroduce;
  destructor Destroy; override;
  procedure SetCount(const iReadCount, iContentLength: Int64);
  property Abort: Boolean read FAbort;
end;


{ TDownloadThread.TProgressThread }

constructor TDownloadThread.TProgressThread.Create(
  const iDownloadThread: TDownloadThread;
  const iProgressTime: Integer);
begin
  inherited Create(True);

  FreeOnTerminate := True;

  FDownloadThread := iDownloadThread;
  FProgressTime := iProgressTime;
  FSynchronizer := TMultiReadExclusiveWriteSynchronizer.Create;
end;

destructor TDownloadThread.TProgressThread.Destroy;
begin
  FSynchronizer.DisposeOf;

  inherited;
end;

procedure TDownloadThread.TProgressThread.Execute;
var
  ReadCount, ContentLength: Int64;
begin
  while (not Terminated) do
  begin
    if (FRunning) then
      Continue;

    FSynchronizer.BeginRead;
    try
      ReadCount := FReadCount;
      ContentLength := FContentLength;
    finally
      FSynchronizer.EndRead;
    end;

    if
      (MilliSecondsBetween(Now, FStart) > FProgressTime) or
      (ReadCount >= ContentLength)
    then
    begin
      FRunning := True;
      try
        TThread.Synchronize(
          Self,
          procedure
          begin
            if (Assigned(FDownloadThread.FOnProgress)) then
              FDownloadThread.FOnProgress(
                FDownloadThread,
                FReadCount,
                FContentLength,
                FAbort);
          end
        );
      finally
        FRunning := False;
      end;

      FStart := Now;
    end;
  end;
end;

procedure TDownloadThread.TProgressThread.SetCount(
  const iReadCount, iContentLength: Int64);
begin
  FSynchronizer.BeginWrite;
  try
    FReadCount := iReadCount;
    FContentLength := iContentLength;
  finally
    FSynchronizer.EndWrite;
  end;
end;

使用する側は↓こんな感じ

procedure TDownloadThread.HttpReceiveData(
  const Sender: TObject;
  iContentLength, iReadCount: Int64;
  var ioAbort: Boolean);
begin
  if (Assigned(FOnProgress)) then
  begin
    FProgressThread.SetCount(iReadCount, iContentLength);
    ioAbort := FProgressThread.Abort;
  end;
end;

使用する側では FProgressThread に読んだバイト数と総バイト数をセット。
以前の実行による Abort 結果を OnReceivedData に返す、ということだけして、すぐ抜けます。
これによって、ダウンロードのオーバーヘッドを最小限に留めます。

TMultiReadExclusiveWriteSynchronizer によるブロックがあるので、若干のオーバーヘッドがあります。
50[msec]の空き時間を取ると速くなるのも、これと関係しています。

まとめ

ということで、TNetHttpClient がクソであり、OnReceivedData の中では Synchronize は好ましくない、と言うことが解りました!

落とし穴解決編

つづくよ!!

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3