Shrinavat 16 Posted January 9, 2019 My task is to draw a lot of small images in a certain order on one large one. I'm using TBitmap32 from Graphics32 Library. Since TBitmap32 is a descendant of TThreadPersistent, it inherits its locking mechanism and it may be used in multi-threaded applications. Here is how I do it now in the main thread: var LargeImage: TBitmap32; procedure BuildLargeBitmap; var x,y, bx,by: integer; AreaRect: TRect; tile: TBitmap32; begin LargeImage.BeginUpdate; try AreaRect := GetSpecifiedArea; // small images (tiles) are 256*256 pixels in size LargeImage.SetSize(256 * (AreaRect.Right - AreaRect.Left+1), 256 * (AreaRect.Bottom - AreaRect.Top+1)); { loop of drawing tiles on a large bitmap } by := 0; // bx,by - coordinates on the bitmap where the tile is drawn (in tiles) for y := AreaRect.Top to AreaRect.Bottom do begin bx := 0; for x := AreaRect.Left to AreaRect.Right do begin tile := GetTileFromDB(x, y); // get tile from the database if Assigned(tile) then // if tile exists, render it on the large bitmap RenderTileToBitmap32(LargeImage, tile, bx*256, by*256); inc(bx); end; inc(by); end; finally LargeImage.EndUpdate; end; end; I want to use all the power of OmniThreadLibrary and bring this rendering loop to a separate thread(s). Which one of the proposed abstractions should I use? ForEach, Fork/Join, Map or something else? Please give me advice and an example applicable to my task. Thanks! Share this post Link to post
Cristian Peța 107 Posted January 9, 2019 If GetTitleFromDB() is using more DB connections and is multi-threaded and if this function is the main bottleneck then it makes sense to parallelize this for loop. Otherwise you can test but I think it will be no gain or even worse because LargeImage will not execute anything in parallel. Looking is per instance and you have only one instance. 1 Share this post Link to post
Guest Posted January 10, 2019 You can use a BlockingCollection from OTL to separate the DB access from the rendering part. While you render a tile the next tile is retrieved from DB in parallel. Share this post Link to post
Shrinavat 16 Posted January 10, 2019 14 hours ago, Cristian Peța said: I think it will be no gain or even worse because LargeImage will not execute anything in parallel. After more thinking I agree with you. I will do without multithreading when creating a single LargeImage. But I have another question. Put my previous code in the BuildLargeBitmap function: function BuildLargeBitmap(some parameters): TBitmap32; begin // creating a large image from tiles end; I need to create multiple different LargeImages in parallel and then merge them into one resulting image. Schematically, as in the attached image. The number of LargeImages is not constant (!) and can be from 1 to 5. Each LargeImage is created from unrelated databases (there are no problems with competing DB connections). If the number of LargeImage is 1, then ResultImage = LargeImage1. Moreover, this operation of building LargeImages and merging them is repeated many times during the application life cycle. That is, I need to have a some "image factory" that is constantly running in the background. I do not understand how to do this using OmniThreadLibrary abstractions. I would be very grateful if someone could help me create a sketch of the code for this task! Share this post Link to post
Guest Posted January 10, 2019 Again, use BlockingCollection from OTL and build a pipeline Share this post Link to post
Shrinavat 16 Posted January 10, 2019 2 hours ago, Schokohase said: Again, use BlockingCollection from OTL and build a pipeline Why should I use a pipeline? And what about ForkJoin abstraction? Here is a quote from the "Parallel Programming with OmniThreadLibrary" book: Quote A typical fork/join usage pattern is: • Execute multiple subtasks. • Wait for subtasks to terminate. • Collect subtask results. • Use results to compute higher-level result. Isn't that for my case (to create multiple different LargeImages in parallel and then merge them into one resulting image)? Share this post Link to post
Guest Posted January 10, 2019 Nope, think about how you would do that in real life: You have to paint small pictures and then put them all together to a big picture. How can you speed it up? Get a friend to paint the small pictures (a task) and put them in a queue (BlockingCollection) from where you take them and put them togehter to the big picture (another task). Share this post Link to post
Shrinavat 16 Posted January 10, 2019 (edited) 2 hours ago, Schokohase said: How can you speed it up? Ok, I get it. That answers my original question, but now I'm curious about my next question. How to implement some kind of "image factory", which is constantly running in the background? It must have a variable number of parallel subtasks to create each image. In my opinion, the "pipeline" abstraction is not suitable for this, since it is a sequential conveyor. Edited January 10, 2019 by Shrinavat Share this post Link to post
Shrinavat 16 Posted January 13, 2019 Dear Primož, please could you give me some advice on how to implement my "image factory"? With a variable number of parallel tasks and constantly working in the background. Everything is as I described above. Please!!! Share this post Link to post
Primož Gabrijelčič 223 Posted January 13, 2019 Pipeline would work as you can run each stage in more than one parallel task (by using .NumTasks). But Parallel.BackgroundWorker is probably more appropriate. 1 Share this post Link to post
Anders Melander 1814 Posted January 13, 2019 (edited) Here's how I would solve it - in theory: Assign each tile a sequential number. Since you know the size of the target bitmap (TargetSizeX*TargetSizeY) and you know the size of each tile (TileSizeX*TileSizeY), calculating the tile number is simple: TileCountX := ((TargetSizeX + TileSizeX-1) div TileSizeX); TileCountY := ((TargetSizeY + TileSizeY-1) div TileSizeY); TileCount := TileCountX * TileCountY; // Tile number goes from 0 to TileCount-1 // Tile coords from Tile number TileX := (TileNumber mod TileCountX) * TileSizeX; TileY := (TileNumber div TileCountY) * TileSizeY; The job of reading a tile from the database can be delegated to one or more tasks, depending on how you choose to partition the workload. A DB tasks reads a request from a (threadsafe) queue, performs the database request, stores the result in the request object and notifies the requestor that the result is ready. The request object contains: 1) Tile number, 2) Result bitmap and 3) Signal object (e.g. an event handle). Create a number of tasks to render the tiles. Each task grabs a Tile Number (just use a InterlockedIncrement() on a shared integer), creates a DB request object, queues it and waits for the result. Once the result is ready the task draws the tile onto the target bitmap (*) and starts over. To avoid cache conflicts it would be best if the Tile Numbers currently being worked on are as far apart as possible, but I guess the DB overhead will make this optimization irrelevant. *) A TBitmap32 is just a chunk of sequential memory and since none of the tile tasks will write to the same memory it is not necessary to lock the target bitmap. So in short: One thread pool to render the tiles and one thread pool to read from the database. A work queue in between them. However like @Cristian Peța said, unless you are using some super fast ninja science fiction database, there's no reason to try to optime the rendering much. All the tiles can probably be rendered in the time it takes to make a single database request. In fact, using graphics32, a thread context switch will take far longer that drawing a single tile. So in practice I would probably just do away with the DB tasks and execute the database request directly from the rendering tasks. Edited January 13, 2019 by Anders Melander 1 Share this post Link to post
Shrinavat 16 Posted January 14, 2019 Thank you for the helpful tips! I will experiment more with these techniques. Share this post Link to post
Cristian Peța 107 Posted January 14, 2019 First you need to know much time takes GetTitleFromDB and RenderTitleToBitmap32. Then you will know where to optimize. For SQL servers hawing many small requests is costly. I don't know how much KB GetTitleFromDB is asking but generally reducing the number of requests (bigger data chunks) is better for client and for server also. 1 Share this post Link to post
Guest Posted January 14, 2019 (edited) @Cristian Peța you can optimize (nearly) every pipeline switching from serial to parallel. # = Read tile from DB * = Render tile Length of pattern represents consuming time Serial: Thread1: ##*##*##*##*##*##*##*##*##*##* Parallel: Thread1: #################### Thread2: * * * * * * * * * * Edited January 14, 2019 by Guest Share this post Link to post
Cristian Peța 107 Posted January 14, 2019 @Schokohase if you are speaking about my suggestion to read bigger chunks from DB then this is not exactly parallelization. And the second suggestion was about what to optimize. If reading from DB takes 10ms and rendering 0.1ms per piece then I would not care about rendering. 1 Share this post Link to post
Shrinavat 16 Posted January 15, 2019 So, I decided to abandon the idea of parallel building a large image from tiles. I coded a small test application for testing my "image factory". Project full source is in attachment, here is the main unit part: unit Unit1; interface uses System.SysUtils, System.Classes, Vcl.Controls, Vcl.Forms, Vcl.StdCtrls, GR32, GR32_Image, GR32_Resamplers, OtlCommon, OtlCollections, OtlParallel; type TForm1 = class(TForm) Image32: TImage32; memLog: TMemo; btnStartTask1: TButton; btnCreateImageFactory: TButton; btnStartTask2: TButton; btnStartTask3: TButton; procedure FormDestroy(Sender: TObject); procedure btnCreateImageFactoryClick(Sender: TObject); procedure btnStartTask1Click(Sender: TObject); procedure btnStartTask2Click(Sender: TObject); procedure btnStartTask3Click(Sender: TObject); private FResultBitmap: TBitmap32; FLogger: IOmniBackgroundWorker; FPipelineImageFactory: IOmniPipeline; strict protected //asynchronous workers procedure Asy_Renderer(const input: TOmniValue; var output: TOmniValue); procedure Asy_Merger(const input, output: IOmniBlockingCollection); end; TTaskRec = record Left, Top, Right, Bottom: integer; TileSource: TFileName; end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.FormDestroy(Sender: TObject); begin FResultBitmap.Free; if Assigned(FPipelineImageFactory) then begin FPipelineImageFactory.Input.CompleteAdding; FPipelineImageFactory.WaitFor(INFINITE); FPipelineImageFactory := nil; end; if Assigned(FLogger) then begin FLogger.Terminate(INFINITE); FLogger := nil; end; end; procedure TForm1.btnCreateImageFactoryClick(Sender: TObject); begin FLogger := Parallel.BackgroundWorker.NumTasks(1) .Execute( procedure(const workItem: IOmniWorkItem) begin memLog.Lines.Add(workItem.Data.AsString); end); FPipelineImageFactory := Parallel.Pipeline .Stage(Asy_Renderer) .Stage(Asy_Merger) .OnStopInvoke( procedure begin Image32.Bitmap.Assign(FResultBitmap); end) .Run; FResultBitmap := TBitmap32.Create(); // buffer FResultBitmap.SetSize(2*256, 2*256); btnCreateImageFactory.Enabled := False; btnStartTask1.Enabled := True; btnStartTask2.Enabled := True; btnStartTask3.Enabled := True; FLogger.Schedule(FLogger.CreateWorkItem('ImageFactory created!')); end; //****************************************************************************** // ImageFactory //****************************************************************************** procedure TForm1.Asy_Renderer(const input: TOmniValue; var output: TOmniValue); var x,y, bx,by: integer; Rec: TTaskRec; tile: TBitmap32; LargeBitmap: TBitmap32; begin // stage I FLogger.Schedule(FLogger.CreateWorkItem('stage I')); Rec := input.ToRecord<TTaskRec>; LargeBitmap := TBitmap32.Create(); LargeBitmap.SetSize(2*256, 2*256); // Large image is 2x2 tiles tile := TBitmap32.Create(); try tile.Font.Size := 24; { loop of drawing tiles to a large bitmap } by := 0; // bx,by - coordinates on the bitmap where the tile is drawn (in tiles) for y := Rec.Top to Rec.Bottom do begin bx := 0; for x := Rec.Left to Rec.Right do begin tile.LoadFromFile(Rec.TileSource); // for test only! In reality it will be like this: tile := GetTileFromDB(x, y) tile.RenderText(10,10, Format('x=%d, y=%d', [x,y]), 0, clTrWhite32); // for info LargeBitmap.Draw(bx*256, by*256, tile); // render the tile to a large image inc(bx); end; inc(by); end; output := LargeBitmap; output.OwnsObject := True; //sleep(Random(2000)); finally tile.Free; end; end; procedure TForm1.Asy_Merger(const input, output: IOmniBlockingCollection); var LargeBitmap: TOmniValue; begin // stage II FLogger.Schedule(FLogger.CreateWorkItem('stage II')); for LargeBitmap in input do begin // merge all LargeBitmaps into one (with alpha channel) BlockTransfer( FResultBitmap, 0, 0, FResultBitmap.ClipRect, TBitmap32(LargeBitmap.AsObject), TBitmap32(LargeBitmap.AsObject).ClipRect, dmBlend); end; FLogger.Schedule(FLogger.CreateWorkItem('ImageFactory task completed!')); end; //****************************************************************************** //****************************************************************************** procedure TForm1.btnStartTask1Click(Sender: TObject); var Rec1: TTaskRec; begin Rec1.Left := 0; // Large image is 2x2 tiles Rec1.Top := 0; Rec1.Right := 1; Rec1.Bottom := 1; Rec1.TileSource := 'base.bmp'; FPipelineImageFactory.NumTasks(1); // to create a single image in parallel // Provide input with FPipelineImageFactory.Input do begin Add(TOmniValue.FromRecord<TTaskRec>(Rec1)); CompleteAdding; end; FLogger.Schedule(FLogger.CreateWorkItem('ImageFactory task #1 started - build one image, no merging')); end; procedure TForm1.btnStartTask2Click(Sender: TObject); var Rec1, Rec2: TTaskRec; begin Rec1.Left := 0; // base Large image is 2x2 tiles Rec1.Top := 0; Rec1.Right := 1; Rec1.Bottom := 1; Rec1.TileSource := 'base.bmp'; Rec2.Left := 0; // overlay #1 Large image Rec2.Top := 0; Rec2.Right := 1; Rec2.Bottom := 1; Rec2.TileSource := 'overlay_1.bmp'; FPipelineImageFactory.NumTasks(2); // to create TWO images in parallel // Provide input with FPipelineImageFactory.Input do begin Add(TOmniValue.FromRecord<TTaskRec>(Rec1)); Add(TOmniValue.FromRecord<TTaskRec>(Rec2)); CompleteAdding; end; FLogger.Schedule(FLogger.CreateWorkItem('ImageFactory task #2 started - build 2 images with merging')); end; procedure TForm1.btnStartTask3Click(Sender: TObject); var Rec1, Rec2, Rec3: TTaskRec; begin Rec1.Left := 0; // base Large image is 2x2 tiles Rec1.Top := 0; Rec1.Right := 1; Rec1.Bottom := 1; Rec1.TileSource := 'base.bmp'; Rec2.Left := 0; // overlay #1 for base Large image Rec2.Top := 0; Rec2.Right := 1; Rec2.Bottom := 1; Rec2.TileSource := 'overlay_1.bmp'; Rec3.Left := 0; // overlay #2 for base Large image Rec3.Top := 0; Rec3.Right := 1; Rec3.Bottom := 1; Rec3.TileSource := 'overlay_2.bmp'; FPipelineImageFactory.NumTasks(3); // to create THREE images in parallel // Provide input with FPipelineImageFactory.Input do begin Add(TOmniValue.FromRecord<TTaskRec>(Rec1)); Add(TOmniValue.FromRecord<TTaskRec>(Rec2)); Add(TOmniValue.FromRecord<TTaskRec>(Rec3)); CompleteAdding; end; FLogger.Schedule(FLogger.CreateWorkItem('ImageFactory task #3 started - build 3 images with merging')); end; end. Screenshots: The result of task #1: The result of task #2: The result of task #3: I'm not sure I did it right. And now I have a bunch of questions, mostly trivial. If you can answer any, please do. 1) Why does stage II start before the creation of ImageFactory pipeline? (see log) 2) Why can't I restart any task? I get "Adding to complete collection" error. 3) How to terminate the pipeline correctly? Sometimes my app remains hanging in task Manager after closing. 4) Are LargeBitmaps created in parallel when there are several (for task #2/#3)? Despite the increase in FPipelineImageFactory.NumTasks for task #2/#3, the number of threads has not changed in the debugger. Also it is noticeable in the log - the line "stage I" appears sequentially. 5) Getting the result - Image32.Bitmap.Assign(FResultBitmap) in OnStopInvoke method - is that right way? If not, how do I get this correctly? 6) Is the overlay order always preserved? That is, for task #3 the overlay_2 over overlay_1 which on base image? with this code: with FPipelineImageFactory.Input do begin Add(TOmniValue.FromRecord<TTaskRec>(Rec1)); Add(TOmniValue.FromRecord<TTaskRec>(Rec2)); Add(TOmniValue.FromRecord<TTaskRec>(Rec3)); CompleteAdding; end; Thanks in advance! test_ImageFactory.zip Share this post Link to post
Shrinavat 16 Posted January 18, 2019 I carefully read the "3.9 Background worker" chapter from the "Parallel Programming with OmniThreadLibrary" book, and - hallelujah! I was able to solve my problem with the help of Parallel.BackgroundWorker and Parallel.ParallelTask. Well, at least I think so... Again, project full source is in attachment, here is the main unit part: unit UBackgroundWorkerImageFactory; interface uses Winapi.Windows, System.SysUtils, System.Classes, Vcl.Controls, Vcl.Forms, Vcl.StdCtrls, GR32, GR32_Image, GR32_Resamplers, OtlCommon, OtlCollections, OtlParallel, OtlSync, OtlTask; type TfrmBackgroundWorkerImageFactory = class(TForm) Image32: TImage32; memLog: TMemo; btnStartTask1: TButton; btnCreateImageFactory: TButton; btnStartTask2: TButton; btnStartTask3: TButton; btnStartTask4: TButton; btnStartTask5: TButton; procedure FormDestroy(Sender: TObject); procedure btnCreateImageFactoryClick(Sender: TObject); procedure btnStartTask1Click(Sender: TObject); procedure btnStartTask2Click(Sender: TObject); procedure btnStartTask3Click(Sender: TObject); procedure btnStartTask4Click(Sender: TObject); procedure btnStartTask5Click(Sender: TObject); private FLogger: IOmniBackgroundWorker; FBackgroundWorkerImageFactory: IOmniBackgroundWorker; // asynchronous workers procedure Asy_Factory(const workItem: IOmniWorkItem); procedure HandleRequestDone(const Sender: IOmniBackgroundWorker; const workItem: IOmniWorkItem); end; var frmBackgroundWorkerImageFactory: TfrmBackgroundWorkerImageFactory; implementation {$R *.dfm} procedure TfrmBackgroundWorkerImageFactory.FormDestroy(Sender: TObject); begin if Assigned(FBackgroundWorkerImageFactory) then begin FBackgroundWorkerImageFactory.CancelAll; FBackgroundWorkerImageFactory.Terminate(INFINITE); FBackgroundWorkerImageFactory := nil; end; if Assigned(FLogger) then begin FLogger.Terminate(INFINITE); FLogger := nil; end; end; procedure TfrmBackgroundWorkerImageFactory.btnCreateImageFactoryClick(Sender: TObject); begin FLogger := Parallel.BackgroundWorker.NumTasks(1) .Execute( procedure(const workItem: IOmniWorkItem) begin memLog.Lines.Add(workItem.Data.AsString); end); FBackgroundWorkerImageFactory := Parallel.BackgroundWorker .Execute(Asy_Factory) .OnRequestDone(HandleRequestDone); btnCreateImageFactory.Enabled := False; btnStartTask1.Enabled := True; btnStartTask2.Enabled := True; btnStartTask3.Enabled := True; btnStartTask4.Enabled := True; btnStartTask5.Enabled := True; FLogger.Schedule(FLogger.CreateWorkItem('ImageFactory created!')); end; //****************************************************************************** // ImageFactory //****************************************************************************** procedure TfrmBackgroundWorkerImageFactory.Asy_Factory(const workItem: IOmniWorkItem); var Left, Top, Right, Bottom: integer; TileSource: TFileName; iTask: integer; numTasks: integer; RenderQueue: IOmniBlockingCollection; Renderer: IOmniParallelTask; TaskResults: array of TBitmap32; ResultBitmap: TBitmap32; begin RenderQueue := TOmniBlockingCollection.Create; numTasks := workItem.Data['LayersCount'].AsInteger + 1; Left := workItem.Data['Left'].AsInteger; Top := workItem.Data['Top'].AsInteger; Right := workItem.Data['Right'].AsInteger; Bottom := workItem.Data['Bottom'].AsInteger; // create multiple TBitmap32, one per child task SetLength(TaskResults, numTasks); for iTask := Low(TaskResults) to High(TaskResults) do TaskResults[iTask] := TBitmap32.Create(2*256, 2*256); // Large image is 2x2 tiles // start child tasks Renderer := Parallel.ParallelTask.NumTasks(numTasks).NoWait .Execute( procedure var workItem: TOmniValue; x,y, bx,by: integer; tile: TBitmap32; begin workItem := RenderQueue.Next; tile := TBitmap32.Create(); try tile.Font.Size := 24; { loop of drawing tiles to a large bitmap } by := 0; // bx,by - coordinates on the bitmap where the tile is drawn (in tiles) for y := Top to Bottom do begin bx := 0; for x := Left to Right do begin tile.LoadFromFile(workItem[1].AsString); // TileSource - for test only! In reality it will be like this: tile := GetTileFromDB(x, y) tile.RenderText(10,10, Format('x=%d, y=%d', [x,y]), 0, clTrWhite32); // for info TaskResults[workItem[0].AsInteger].Draw(bx*256, by*256, tile); // render tile to a large bitmap (output: index of Bitmap32 in taskResults array) inc(bx); FLogger.Schedule(FLogger.CreateWorkItem( Format('processed %s: x=%d, y=%d (thread=%d)', [workItem[1].AsString,x,y,GetCurrentThreadID]))); //Sleep(500); // simulate workload end; inc(by); end; finally tile.Free; end; end ); // provide input to child tasks for iTask := 0 to numTasks-1 do begin TileSource := workItem.Data['TileSource' + iTask.ToString]; RenderQueue.Add(TOmniValue.Create([iTask, TileSource])); end; // process output Renderer.WaitFor(INFINITE); if not workItem.CancellationToken.IsSignalled then begin FLogger.Schedule(FLogger.CreateWorkItem(Format('Merging (thread=%d)', [GetCurrentThreadID]))); ResultBitmap := TBitmap32.Create(); ResultBitmap.Assign(TaskResults[0]); // base image only // if layers exists for iTask := 1 to High(TaskResults) do begin // merge all Layers over base image (with alpha channel) BlockTransfer( ResultBitmap, 0, 0, ResultBitmap.ClipRect, TaskResults[iTask], TaskResults[iTask].ClipRect, dmBlend); end; workItem.Result := ResultBitmap; // workItem.Result.OwnsObject := True; end; for iTask := Low(TaskResults) to High(TaskResults) do TaskResults[iTask].Free; end; procedure TfrmBackgroundWorkerImageFactory.HandleRequestDone( const Sender: IOmniBackgroundWorker; const workItem: IOmniWorkItem); begin Image32.Bitmap.Assign(workItem.Result.AsObject as TBitmap32); workItem.Result.AsObject.Free; FLogger.Schedule(FLogger.CreateWorkItem('ImageFactory task completed!')); end; //****************************************************************************** //****************************************************************************** procedure TfrmBackgroundWorkerImageFactory.btnStartTask1Click(Sender: TObject); var ov: TOmniValue; begin // create a single image in parallel ov := TOmniValue.CreateNamed( ['Left', 0, 'Top', 0, 'Right', 1, 'Bottom', 1, 'LayersCount', 0, 'TileSource0', 'base.bmp' ]); FBackgroundWorkerImageFactory.Schedule(FBackgroundWorkerImageFactory.CreateWorkItem(ov)); FLogger.Schedule(FLogger.CreateWorkItem('ImageFactory task #1 started - build one image, no merging')); end; procedure TfrmBackgroundWorkerImageFactory.btnStartTask2Click(Sender: TObject); var ov: TOmniValue; begin // create and merge TWO images in parallel ov := TOmniValue.CreateNamed( ['Left', 0, 'Top', 0, 'Right', 1, 'Bottom', 1, 'LayersCount', 1, 'TileSource0', 'base.bmp', 'TileSource1', 'overlay_1.bmp' ]); FBackgroundWorkerImageFactory.Schedule(FBackgroundWorkerImageFactory.CreateWorkItem(ov)); FLogger.Schedule(FLogger.CreateWorkItem('ImageFactory task #2 started - build 2 images with merging')); end; procedure TfrmBackgroundWorkerImageFactory.btnStartTask3Click(Sender: TObject); var ov: TOmniValue; begin // create and merge THREE images in parallel ov := TOmniValue.CreateNamed( ['Left', 0, 'Top', 0, 'Right', 1, 'Bottom', 1, 'LayersCount', 2, 'TileSource0', 'base.bmp', 'TileSource1', 'overlay_1.bmp', 'TileSource2', 'overlay_2.bmp' ]); FBackgroundWorkerImageFactory.Schedule(FBackgroundWorkerImageFactory.CreateWorkItem(ov)); FLogger.Schedule(FLogger.CreateWorkItem('ImageFactory task #3 started - build 3 images with merging')); end; procedure TfrmBackgroundWorkerImageFactory.btnStartTask4Click(Sender: TObject); var ov: TOmniValue; begin // create and merge FOUR images in parallel ov := TOmniValue.CreateNamed( ['Left', 0, 'Top', 0, 'Right', 1, 'Bottom', 1, 'LayersCount', 3, 'TileSource0', 'base.bmp', 'TileSource1', 'overlay_1.bmp', 'TileSource2', 'overlay_2.bmp', 'TileSource3', 'overlay_3.bmp' ]); FBackgroundWorkerImageFactory.Schedule(FBackgroundWorkerImageFactory.CreateWorkItem(ov)); FLogger.Schedule(FLogger.CreateWorkItem('ImageFactory task #4 started - build 4 images with merging')); end; procedure TfrmBackgroundWorkerImageFactory.btnStartTask5Click(Sender: TObject); var ov: TOmniValue; begin // create and merge FOUR images in parallel (reversed layers order of Task #4) ov := TOmniValue.CreateNamed( ['Left', 0, 'Top', 0, 'Right', 1, 'Bottom', 1, 'LayersCount', 3, 'TileSource0', 'base.bmp', 'TileSource1', 'overlay_3.bmp', 'TileSource2', 'overlay_2.bmp', 'TileSource3', 'overlay_1.bmp' ]); FBackgroundWorkerImageFactory.Schedule(FBackgroundWorkerImageFactory.CreateWorkItem(ov)); FLogger.Schedule(FLogger.CreateWorkItem('ImageFactory task #5 started - reversed Task #4')); end; end. Everything running like clockwork. But I have a few questions again: 1) If I change numTasks value in Renderer := Parallel.ParallelTask.NumTasks(numTasks).NoWait line to Environment.Process.Affinity.Count - 1, the application starts to work incorrectly. The code after Renderer.WaitFor(INFINITE); line is never executed and my app remains hanging in task Manager after closing. Is this my bug or OTL bug? 2) If I uncomment workItem.Result.OwnsObject := True; line in Asy_Factory procedure and comment out workItem.Result.AsObject.Free; line in HandleRequestDone procedure, I obtain the following AV: "Access violation at address 00604CBF in module 'ImageFactory_BackgroundWorker.exe'. Read of address 00000000." Why? After all, TOmniValue is the owner of TObject-type data! When a object-owning TOmniValue goes out of scope, the owned object is automatically destroyed. But this is not so in my case, so I would like to know why it happens. Is this OTL bug? Dear @Primož Gabrijelčič, please could you answer these questions? Also, your opinion on my ImageFactory code is very important to me! Especially the asynchronous part of the code. Maybe is there a better/easier/rather way to do this? test_ImageFactory_BackgroundWorker.zip Share this post Link to post
Shrinavat 16 Posted January 30, 2019 Questions from my last post are still relevant 10+ days later. Dear @Primož Gabrijelčič, please reply! PS If this OTL subforum is not intended for questions, I am sorry for the time I have wasted. Is there another forum that I can ask specific OTL question and get an answer from the developer? Share this post Link to post
Primož Gabrijelčič 223 Posted January 30, 2019 I see accvio in G32.pas in procedure TCustomBitmap32.ChangeSize(var Width, Height: Integer; NewWidth, NewHeight: Integer); begin FBackend.ChangeSize(Width, Height, NewWidth, NewHeight); end; FBackend is nil Sorry, no idea why. Share this post Link to post
Anders Melander 1814 Posted January 30, 2019 I believe this was addressed recently in Graphics32: TBitmap32 constructor access violation FWIW @Shrinavat you should have been able to determine the cause of the AV on your own by just looking at the call stack in the debugger. If this is beyond you then multi threading is not for you. Share this post Link to post
Primož Gabrijelčič 223 Posted January 31, 2019 11 hours ago, Anders Melander said: I believe this was addressed recently in Graphics32: TBitmap32 constructor access violation TNX. I have to update, obviously 🙂 Share this post Link to post
Shrinavat 16 Posted January 31, 2019 (edited) @Anders Melander I have latest Graphics32 with fixed TBitmap32 constructor access violation AV occurs when executing a line of code Image32.Bitmap.Assign(workItem.Result.AsObject as TBitmap32); - FBackend is nil: This is in case when workItem.Result.Ownsobjects: = True If I comment out workItem.Result.OwnsObject := True line in Asy_Factory procedure and uncomment workItem.Result.AsObject.Free; line in HandleRequestDone procedure, FBackend is not nil. It's a bug. But whose? Mine, GR32 or OTL? Edited January 31, 2019 by Shrinavat Share this post Link to post
Primož Gabrijelčič 223 Posted January 31, 2019 Indeed, if you set `workItem.Result` as owned object, it gets destroyed immediately after the `Asy_Execute` exits. This is the minimal code that reproduces the problem: procedure TfrmBackgroundWorkerImageFactory.Asy_Factory(const workItem: IOmniWorkItem); begin if not workItem.CancellationToken.IsSignalled then workItem.Result.AsOwnedObject := TBitmap32.Create(256,256); end; At the moment, don't use `OwnsObject` or `AsOwnedObject` at that point. I'll dig in to see what's going on. 1 Share this post Link to post
Primož Gabrijelčič 223 Posted January 31, 2019 Minimized problem: Maybe somebody can tell me what is going on because I'm clueless. Share this post Link to post
Primož Gabrijelčič 223 Posted January 31, 2019 `workItem.Result` is a record property. Because of that, this code: workItem.Result.AsOwnedObject := TBitmap32.Create(256,256); is equivalent to running: var tmp: TOmniValue; tmp := workItem.Result; tmp.AsOwnedObject := TBitmap32.Create(256,256); And that fails. You should change the code to: var tmp: TOmniValue; tmp.AsOwnedObject := TBitmap32.Create(256,256); workItem.Result := tmp; I'll see if I can change the implementation of `IOmniWorkitem` to prevent such problems. 1 Share this post Link to post