FMX-TBitmapはThread-safe?


sh  2025-02-26 21:29:47  No: 151803

投稿するかどうか少し迷ったのですが、最近気になってたので敢えて投稿しました。
前提として、TokyoあたりからFMX-TBitmapはThread-safeとの認識があります。

やってることは、バックグラウンド・スレッド内でネットにアクセスして画像ファイルを取得し。これをTBitmapにロードし、場合によっては少々加工し、Synchronizeを使ってUIスレッドでTImage.Bitmapに割り当てて表示するという単純なものです。ファイル取得は数秒間隔でトリガされ、24時間/365日の連続運用を前提としています。

TTask.Run(procedure 
begin
 HTTPClient.Get(URL, Stream);
 Bitmap.LoadFromStream(Stream);
 // 必要であれば加工
 ...
 // UI更新
 TThread.Synchronize(nil, procedure
 begin
  Image1.Bitmap := Bitmap;
 end);
end);

Delphi 11.4(CE)では長期間運用でもとくに問題なかったんですが、Delphi 12.1(CE)でBuildすると、数時間もするとエラーが発生してしまいます。どうもTBitmapに関連してTMessageManagerやTMonitor関連でアクセスエラーが発生しているようです。

ネットで検索すると、多くの場合にFMX-TBitmapはThread-safeではないとの見解がちらほら。
ネットの上のアドバイスに従って、コードはそのままでSkiaに変えることで、エラーは皆無になりました(Skia自体、コントロールやPlattformによって、少々表示上の不具合があるのですが、対処できる範囲なのでOKということで)。
 GlobalUseSkia := True;
 GlobalSkiaBitmapsInParallel := True;

①そもそもFMX-TBitmapはThread-safeではない?
②Thread-safeだが、使い方が悪い?
③Delphi 12.1固有のの問題?

よろしくお願いします。


vram  2025-02-27 10:19:58  No: 151816

Threadはデバッグが難しくてエラーが出ると原因を調べるのに時間がかかるのですが
最小限のプログラムを作成してそれを連続で実行してみてどうなるでしょうか?

それと質問と内容がちょっとズレていますがそのサンプルの場合
TBitmapがスレッドセーフなのか?ではなくTImageがスレッドセーフかどうかではないでしょうか?

TListViewやTTreeViewとTImageListを連携して使用する場合
TImageListに画像を読み込んで追加するのは時間がかかるのでスレッドで処理するのですが
そのTImageListに関連して反映させるTListViewやTTreeViewがスレッドセーフではないので同期をとって反映させています

一見すると正しいように思える処理でもスレッドによるエラーが出ることが多く
Sleepや更新速度の調整、それでもダメなら作り直すなどしてエラーが出なくなるまで調整するしかありません


mam  URL  2025-02-27 10:22:49  No: 151817

とても外していたらすいません。
TTask.Runがどのように呼ばれているかによるのですが、

例1:Stream、HTTPClient、Bitmapをローカルで作成する
TTask.Run(procedure 
var LocalHTTPClient:THTTPClient;
    LocalBitmap:TBitmap;
    LocalStream:TMemoryStream;
begin
  LocalHTTPClient := THTTPClient.Create;
  LocalBitmap:=TBitmap.Create;
  try
    LocalStream:=TMemoryStream.Create;
    LocalHTTPClient.Get(URL, LocalStream);
    LocalBitmap.LoadFromStream(LocalStream);
    // 必要であれば加工
    ...
    // UI更新
    TThread.Synchronize(nil, procedure
    begin
      Image1.Bitmap := LocalBitmap;
    end);
  finally
    LocalHTTPClient.Free;
    LocalBitmap.Free;
  end;
end);

例2:Stream、HTTPClient、Bitmapをロックする
TTask.Run(procedure 
begin
  System.MonitorEnter(Stream);
  System.MonitorEnter(HTTPClient);
  HTTPClient.Get(URL, Stream);
  System.MonitorExit(HTTPClient);
  System.MonitorEnter(Bitmap);
  Bitmap.LoadFromStream(Stream);
  System.MonitorExit(Stream);
  // 必要であれば加工
  ...
  // UI更新
  TThread.Synchronize(nil, procedure
  begin
    Image1.Bitmap := Bitmap;
  end);
  System.MonitorExit(Bitmap);
end);

例3:全てSynchronize内で実行する(TTaskの意味がなくなる)
TTask.Run(procedure 
begin
  TThread.Synchronize(nil, procedure
  begin
    HTTPClient.Get(URL, Stream);
    Bitmap.LoadFromStream(Stream);
    // 必要であれば加工
    ...
    // UI更新
    Image1.Bitmap := Bitmap;
  end);
end);

TTask.Runの呼び方によっては URL:String も気になります。

並列プログラミングにおいては 複数のタスクが開始する順序と終了する順序は保証されません。
TTaskやITaskを実際に実用的に使おうとするとTTaskの派生クラスを作って使用せざるを得なくなります。
例えば以下のソースコードの場合、
1個目のTTask.Runと2個目のTTask.Runが順番に実行開始されるとは限らないのです。
1個目のTTask.Runと2個目のTTask.Runが順番に実行終了するとも限りません。
よって、'1回目'、'2回目'の順に表示される可能性もありますし、
'2回目'、'1回目'の順に表示される可能性もありますし、
'2回目'、'2回目'の順に表示される可能性もあります。

var URL:String
begin
  URL:='1回目';
  TTask.Run(
    procedure
    begin
      ShowMessage(URL)+
    end
  );

  URL:='2回目';
  TTask.Run(
    procedure
    begin
      ShowMessage(URL)+
    end
  );
end;

参考URL
https://mam-mam.net/delphi/vcl_ttask.html

間違っていたら、誠に申し訳ございません。


mam  2025-02-27 10:51:49  No: 151818

後の例は、Synchronizeを入れるべきでした。また、誤りがありました。
誠に申し訳ございません。

var URL:String
begin
  URL:='1回目';
  TTask.Run(
    procedure
    begin
      TThread.Synchronize(procedure
      begin
        ShowMessage(URL);
      end);
    end
  );

  URL:='2回目';
  TTask.Run(
    procedure
    begin
      TThread.Synchronize(procedure
      begin
        ShowMessage(URL);
      end);
    end
  );
end;


mam  URL  2025-02-27 11:24:02  No: 151819

何度もお節介ですいません。
TTaskを実用的に使用しようとすると、色々と多くの考慮しなければならない点があります。

例えば「並列プログラミングでのスレッドプーリングの概要」
https://docwiki.embarcadero.com/Support/ja/%E4%B8%A6%E5%88%97%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%A7%E3%81%AE%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89%E3%83%97%E3%83%BC%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%AE%E6%A6%82%E8%A6%81

にあるように、
TThreadPool.Default.MaxWorkerThreads; で最大スレッド数も配慮しなければなりません。
最大スレッド数を超えるTTask.Runが呼び出されると、どうなるのかはわからないのですが、

例えば「人物写真から目の領域、顔の領域座標を取得(Haar Cascade) ~Delphiソースコード集」
https://mam-mam.net/delphi/haar_cascade.html
では、
MaxWorkerThreadsCount:=TThreadPool.Default.MaxWorkerThreads;
として、最大スレッド数を取得しMaxWorkerThreadsCount個を超えるTTaskを使用しないようにしています。

数個のTTaskを使用するだけなら問題ないのですが、大量に使用する場合は配慮が必要です。

今般の場合は関係ないかもですが、誠にお節介ですいません。


AAAAA  2025-02-27 19:56:56  No: 151830

unit FMX.Graphics;

procedure TBitmap.LoadFromStream(Stream: TStream);
var
  S: TStream;
  Surf: TBitmapSurface;
begin
  TMonitor.Enter(Self);
  try

になってるから一応対応はしているんでないかい?


mam  2025-02-27 20:47:21  No: 151831

AAAAA様

なるほどです。
TBitmap.LoadFromStreamはロックされていても、Bitmapを加工している箇所はロックされていないかもですね。


sh  2025-02-27 21:09:07  No: 151832

vramさん、ありがとうございます。

>TBitmapがスレッドセーフなのか?ではなくTImageがスレッドセーフかどうかではないでしょうか?

えっと、TImageはThread-safeじゃないです。Synchronize内でUIスレッドにアクセスする場合も、UI要素(この場合はTImage)がThread-safeかどうかが重要になるということでしょうか?


sh  2025-02-27 21:12:56  No: 151833

mamさん、ありがとうございます。

・使い方は「例1」とほぼ同じです。

・URLは都度URL生成関数を呼び出し、そこでは時刻からURLを生成しています。

・この画像取得部分の処理はここだけです。
が、よく考えると、このTTask内の処理が終了する前に、この画像処理関数を呼んでしまうことはあるかもです。

・スレッドの最大数はコア数×25がデフォルトの値とのことですが、そこまでは使ってません。

---------------------

function CreateURL: string
begin
  // ...
end;

procedure TForm1.GetImage;
begin
  // ここで少しUI処理

  TTask.Run(
    procedure
    var
      HTTPClient: THTTPClient;
      Response: IHTTPResponse;
      Stream: TMemoryStream;
      ABitmap: TBitmap;
      BitmapData: TBitmapData;
      Line: PAlphaColorArray;
      AColor: TAlphaCOlorRec;
    begin
      HTTPClient := THTTPClient.Create;
      Stream := TMemoryStream.Create;
      ABitmap := TBitmap.Create;
      try
        {$IFDEF MSWINDOWS}
        HTTPClient.AcceptEncoding := 'gzip deflate';
        HTTPClient.AutomaticDecompression := [THTTPCompressionMethod.GZip, THTTPCompressionMethod.Deflate];
        {$ENDIF}

        try
          Response := HTTPClient.Get(CreateURL, Stream);
          if (Response.StatusCode = 200) and (Stream.Size > 0) then begin
            ABitmap.LoadFromStream(Stream);

            if ABitmap.Map(TMapAccess.ReadWrite, BitmapData) then begin
              try
                for var Y := 0 to BitmapData.Height - 1 do begin
                  Line := BitmapData.GetScanline(Y);
                  for var X := 0 to BitmapData.Width - 1 do begin
                    AColor.Color := Line^[X];
                    //ここで画像処理

                    Line^[X] := AColor.Color;
                  end;
                end;
              finally
                ABitmap.Unmap(BitmapData);
              end;
            end;

            TThread.Synchronize(nil, procedure begin
              Image1.Bitmap := ABitmap;
            end);
          end;
        except

        end;
      finally
        HTTPClient.Free;
        Stream.Free;
        ABitmap.Free;
      end;
    end);
end;


AAAAA  2025-02-27 21:57:32  No: 151834

Canvas にも Lock はある

class procedure TCanvas.Lock;
begin
  if not (TCanvasStyle.DisableGlobalLock in GetCanvasStyle) then
    TMonitor.Enter(FLock);
end;

class procedure TCanvas.Unlock;
begin
  if not (TCanvasStyle.DisableGlobalLock in GetCanvasStyle) then
    TMonitor.Exit(FLock);
end;


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加