I need to create a certain number of iTasks to perform operations in a dynamic array and other fields in a record. Each iTask operates in a specific portion of this array. The array is a field in a record which is passed as a var parameter to the iTask.
The operations in the array field is going well, however the other record fields don't return any value after all tasks finish their work. I had help from Dalija in another question that operates only on the array and it worked, but now I'm having trouble with other fields.
This is my code :
program ProjectTest;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
System.Threading;
type
myrec = record
vet: array of integer;
total: integer;
average: integer;
end;
// (1) ===> here is the major procedure that populates the dyn. array and
// calculates other two fields : myrec.total and myrec.avg
procedure ProcA(const pin, pfin: integer; var Prec: myrec);
var
vind: integer;
begin
for vind := pin to pfin do
begin
Prec.vet[vind] := vind * 10;
Prec.total := Prec.total + Prec.vet[vind]; // sum all array values
end;
Prec.average := Trunc(Prec.total / Length(Prec.vet)); // calculates the average
end;
// (2) Here iTask is created and calls ProcA
function CreateTask(first, last: integer; var Pmyrec: myrec): ITask;
var
mylocalrec: myrec;
begin
mylocalrec := Pmyrec;
Result := TTask.Create(
procedure
begin
ProcA(first, last, mylocalrec)
end);
end;
procedure Test;
var
Recarray: myrec;
Ptasks: array of ITask;
vind, indtask, vslice: integer;
vfirst, vlast, vthreads, vsize: integer;
begin
vthreads := 4;
vsize := 16;
SetLength(Ptasks, vthreads);
SetLength(Recarray.vet, vsize);
// Initialize the array , just to check after iTask execution
for vind := low(Recarray.vet) to high(Recarray.vet) do
Recarray.vet[vind] := -33;
// initialize the sum and average field just to check after iTask execution
Recarray.total := -1;
Recarray.average := -2;
// portion of array to scan for each iTask
vslice := Length(Recarray.vet) div vthreads;
for indtask := low(Ptasks) to high(Ptasks) do
begin
vfirst := indtask * vslice;
vlast := (indtask + 1) * vslice - 1;
if (Length(Recarray.vet) mod vthreads <> 0) and (indtask = high(Ptasks)) then vlast := high(Recarray.vet);
Ptasks[indtask] := CreateTask(vfirst, vlast, Recarray);
end;
// Starting all Tasks
for indtask := low(Ptasks) to high(Ptasks) do
Ptasks[indtask].Start;
// Waits for all Tasks been concluded
TTask.WaitForAll(Ptasks);
// (3) Here it is listed the array contents and it is ok
for vind := low(Recarray.vet) to high(Recarray.vet) do
Writeln(' Array position : ' + Format('%.3d', [vind]) + ' content : ' + Recarray.vet[vind].tostring);
Writeln(' =========================================================');
// (4) Here is is listed fields recarray.total and recarray.avg and they were not
// processed inside the iTask . I expected to see the computed values for those fields
Writeln(' Array sum : ' + Format('%.0d', [Recarray.total]) + ' Array average : ' + Format('%5.2n', [Recarray.average * 1.0]));
end;
begin
Test;
Readln;
end.
And the output is :
Array position : 000 content : 0
Array position : 001 content : 10
Array position : 002 content : 20
Array position : 003 content : 30
Array position : 004 content : 40
Array position : 005 content : 50
Array position : 006 content : 60
Array position : 007 content : 70
Array position : 008 content : 80
Array position : 009 content : 90
Array position : 010 content : 100
Array position : 011 content : 110
Array position : 012 content : 120
Array position : 013 content : 130
Array position : 014 content : 140
Array position : 015 content : 150
=========================================================
Array sum : -1 Array average : -2,00
The problem is: after running all the iTasks, only the dynamic array field recarray.vet
contain correct values. The fields recarray.total
and recarray.average
still contain their initial values as before the iTask have run.
How to correctly update values in those fields, so they will contain correct values after tasks finish running?
While it seems that your code has only one problem - how to populate integer fields in a record with tasks - when you solve that one you will have another issue - reading and writing same memory location from multiple threads.
1. How to populate integer fields in a record?
Records are value types and dynamic arrays are reference types. That is the reason why your code can update values in array but it cannot update values in record.
Let's see what is happening here.
function CreateTask(first, last: integer; var pmyrec: myrec): ITask;
var
mylocalrec: myrec;
begin
mylocalrec := pmyrec;
Since mylocalrec
and pmyrec
are records (value types) their content will occupy two different memory locations. Above assignment will copy the content of the pmyrec
into mylocalrec
.
That will be equivalent of following:
mylocalrec.vet := pmyrec.vet;
mylocalrec.total := pmyrec.total;
mylocalrec.average := pmyrec.average;
Since total
and average
are also value types their content will be copied and from this point on any change to either of those integer fields in mylocalrec
will not have any effect to the original pmyrec
. This is why your code is failing.
Why assigning dynamic array field vet
from pmyrec
to mylocalrec
works?
Because dynamic arrays are reference types - assignment from one variable to another only copies value of the reference (pointer) and not the actual content. Both vet
variables will point to the same array you have allocated before you started running your tasks.
To solve above issue you have to pass some reference type instead of value type. The simplest thing is declaring record pointer type and passing that one.
type
myrec = record
vet: array of integer;
total: integer;
average: integer;
end;
pmyrec = ^myrec;
function CreateTask(first, last: integer; rec: pmyrec): ITask;
var
mylocalrec: pmyrec;
begin
mylocalrec := rec;
Result := TTask.Create(
procedure
begin
ProcA(first, last, mylocalrec^)
end);
end;
...
Ptasks[indtask] := CreateTask(vfirst, vlast, @Recarray);
2. How to solve threading issue?
After you have solved original issue you will experience threading issue. Reading and writing same memory location from multiple threads is not safe. Results you will get may be incorrect. While you can run your code thousand times and all values might be correct, sooner or later you will run into situation where they will not.
Populating dynamic array is safe in your case, because each thread operates on distinct part of the array and the array is not reallocated (its size is not changed) while your tasks run. That part of your code is thread safe.
Calculating total
and average
is not thread safe.
You have to synchronize such code with the main thread - in that case all reading and writing will be done from main thread and you will get correct results. Or you have to run such code after all tasks are finished. Whichever is more suitable for particular case.
procedure ProcA(const pin, pfin: integer; prec: pmyrec);
var
vind: integer;
total: integer;
begin
total := 0;
for vind := pin to pfin do
begin
prec.vet[vind] := vind * 10;
total := total + prec.vet[vind]; // sum all array values
end;
TThread.Synchronize(nil,
procedure
begin
prec.total := prec.total + total;
prec.average := Trunc(prec.total / Length(prec.vet)); // calculates the average
end);
end;
However, synchronizing with the main thread within the tasks will cause the deadlock with TTask.WaitForAll
method that also runs from the main thread in your case. To solve that part, you also have to run whole Test
method from another thread.
TTask.Run(
procedure
begin
Test;
end);
And when we put all those parts together, complete code would be:
program ProjectTest;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
System.Classes,
System.Threading;
type
myrec = record
vet: array of integer;
total: integer;
average: integer;
end;
pmyrec = ^myrec;
// (1) ===> here is the major procedure that populates the dyn. array and
// calculates other two fields : myrec.total and myrec.avg
procedure ProcA(const pin, pfin: integer; prec: pmyrec);
var
vind: integer;
total: integer;
begin
total := 0;
for vind := pin to pfin do
begin
prec.vet[vind] := vind * 10;
total := total + prec.vet[vind]; // sum all array values
end;
TThread.Synchronize(nil,
procedure
begin
prec.total := prec.total + total;
prec.average := Trunc(prec.total / Length(prec.vet)); // calculates the average
end);
end;
// (2) Here iTask is created and calls ProcA
function CreateTask(first, last: integer; rec: pmyrec): ITask;
var
mylocalrec: pmyrec;
begin
mylocalrec := rec;
Result := TTask.Create(
procedure
begin
ProcA(first, last, mylocalrec);
end);
end;
procedure Test;
var
Recarray: myrec;
Ptasks: array of ITask;
vind, indtask, vslice: integer;
vfirst, vlast, vthreads, vsize: integer;
begin
vthreads := 4;
vsize := 16;
SetLength(Ptasks, vthreads);
SetLength(Recarray.vet, vsize);
// Initialize the array , just to check after iTask execution
for vind := low(Recarray.vet) to high(Recarray.vet) do
Recarray.vet[vind] := -33;
// initialize the sum and average field just to check after iTask execution
Recarray.total := -1;
Recarray.average := -2;
// portion of array to scan for each iTask
vslice := Length(Recarray.vet) div vthreads;
for indtask := low(Ptasks) to high(Ptasks) do
begin
vfirst := indtask * vslice;
vlast := (indtask + 1) * vslice - 1;
if (Length(Recarray.vet) mod vthreads <> 0) and (indtask = high(Ptasks)) then vlast := high(Recarray.vet);
Ptasks[indtask] := CreateTask(vfirst, vlast, @Recarray);
end;
// Starting all Tasks
for indtask := low(Ptasks) to high(Ptasks) do
Ptasks[indtask].Start;
// Waits for all Tasks been concluded
TTask.WaitForAll(Ptasks);
// (3) Here it is listed the array contents and it is ok
for vind := low(Recarray.vet) to high(Recarray.vet) do
Writeln(' Array position : ' + Format('%.3d', [vind]) + ' content : ' + Recarray.vet[vind].tostring);
Writeln(' =========================================================');
// (4) Here is is listed fields recarray.total and recarray.avg and they were not
// processed inside the iTask . I expected to see the computed values for those fields
Writeln(' Array sum : ' + Format('%.0d', [Recarray.total]) + ' Array average : ' + Format('%5.2n', [Recarray.average * 1.0]));
end;
begin
TTask.Run(
procedure
begin
Test;
end);
end.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With