投稿するかどうか少し迷ったのですが、最近気になってたので敢えて投稿しました。
前提として、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固有のの問題?
よろしくお願いします。
Threadはデバッグが難しくてエラーが出ると原因を調べるのに時間がかかるのですが
最小限のプログラムを作成してそれを連続で実行してみてどうなるでしょうか?
それと質問と内容がちょっとズレていますがそのサンプルの場合
TBitmapがスレッドセーフなのか?ではなくTImageがスレッドセーフかどうかではないでしょうか?
TListViewやTTreeViewとTImageListを連携して使用する場合
TImageListに画像を読み込んで追加するのは時間がかかるのでスレッドで処理するのですが
そのTImageListに関連して反映させるTListViewやTTreeViewがスレッドセーフではないので同期をとって反映させています
一見すると正しいように思える処理でもスレッドによるエラーが出ることが多く
Sleepや更新速度の調整、それでもダメなら作り直すなどしてエラーが出なくなるまで調整するしかありません
とても外していたらすいません。
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
間違っていたら、誠に申し訳ございません。
後の例は、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;
何度もお節介ですいません。
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を使用するだけなら問題ないのですが、大量に使用する場合は配慮が必要です。
今般の場合は関係ないかもですが、誠にお節介ですいません。
unit FMX.Graphics;
procedure TBitmap.LoadFromStream(Stream: TStream);
var
S: TStream;
Surf: TBitmapSurface;
begin
TMonitor.Enter(Self);
try
になってるから一応対応はしているんでないかい?
AAAAA様
なるほどです。
TBitmap.LoadFromStreamはロックされていても、Bitmapを加工している箇所はロックされていないかもですね。
vramさん、ありがとうございます。
>TBitmapがスレッドセーフなのか?ではなくTImageがスレッドセーフかどうかではないでしょうか?
えっと、TImageはThread-safeじゃないです。Synchronize内でUIスレッドにアクセスする場合も、UI要素(この場合はTImage)がThread-safeかどうかが重要になるということでしょうか?
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;
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;