これは 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 専用スレッドのソースが↓こちらです。
```delphi: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 は好ましくない、と言うことが解りました!
#落とし穴解決編
つづくよ!!