THttpClient の落とし穴２


これは [Delphi Advent Calendar](http://qiita.com/advent-calendar/2015/delphi) 補欠の記事です。

#THttpClient とはなんぞや
これについては、[落とし穴１](http://qiita.com/items/998547937c7d05d34977)をご覧下さい！

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

> Android の場合はメインスレッドが５～１０秒停止すると、[ANR（Application Not Response）で落とされちゃいます](http://developer.android.com/intl/ja/training/articles/perf-anr.html)しね！


#TNetHttpClient の実装

僕が、かたくなに [TNetHttpClient](http://docwiki.embarcadero.com/Libraries/Seattle/ja/System.Net.HttpClientComponent.TNetHTTPClient) を使わないのは TNetHttpClient が残念実装だからです。
具体的なコードをちょっと見てみましょう。

```delphi
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](http://docwiki.embarcadero.com/Libraries/Seattle/ja/System.Net.HttpClient.THTTPClient.Get) はサーバの応答を待って、処理が返ってきます。
落とし穴１にも書きましたがブロッキング動作です。
この実装では、Get を呼んだ段階で、メインスレッドが停止します／(^o^)＼
当然、ぐるぐるアニメーションも停止します！！

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

ということで、TNetHttpClient は使い物になりません。
実際、[HttpDownload というエンバカデロが提供しているサンプルコード](http://docwiki.embarcadero.com/CodeExamples/Seattle/en/RTL.HttpDownload_Sample)では、TNetHttpClient を使ってません！
僕と同じように TThread を使って処理を回しています。

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

#OnReceiveData で Synchronize しない！

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

```delphi
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](http://docwiki.embarcadero.com/Libraries/Seattle/ja/System.Classes.TThread.Synchronize) で OnReceiveData を呼び出してますよね。
この実装は一見正しいのです。
というのは、UI の描画はメインスレッドで行う必要があり、[THttpClinet.OnReceivedData](http://docwiki.embarcadero.com/Libraries/Seattle/ja/System.Net.HttpClient.THTTPClient.OnReceiveData) は別スレッドのままやってくるからです。
なので、Synchronize でメインスレッドと同期して、メインスレッドで UI を描画する訳です。

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

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

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

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

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

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

> ```delphi
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;
```

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

では、ということで 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;
```

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

```delphi
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](http://docwiki.embarcadero.com/Libraries/Seattle/ja/System.SysUtils.TMultiReadExclusiveWriteSynchronizer) によるブロックがあるので、若干のオーバーヘッドがあります。
> 50[msec]の空き時間を取ると速くなるのも、これと関係しています。

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

#落とし穴解決編
[つづくよ！！](http://qiita.com/items/95dfebdb659b30918196)