Jump to content
ertank

Changing label text in thread leads to weird display

Recommended Posts

Hello,

 

I am using Delphi 10.3.3 to build an Android 32Bit app running on embedded device with Android 5.1.1 installed. Device has 32Bit CPU and I cannot try to see if building my app for 64Bit helps.

 

There are several operations that app doing in a thread which total takes 6-10 seconds on average. In order to keep user patient, I put a label on screen showing operation is advancing step by step. That label is updated from inside a thread. Thread is started using "TThread.CreateAnonymousThread(<relevant procedure calling code here>).Start()" and Label update code is something as following:

TThread.Synchronize(nil, procedure begin StatusUpdateLabel.Text := '1/' + MaxSteps.ToString(); StatusUpdateLabel.Repaint(); end);

My initial code did not have that "StatusUpdateLabel.Repaint();" part in above code. Once I started to have that problem, I just try to see if it helps. My case it did not help at all. Label is placed on a TFrame and aligned to Top. Font Family is Default, Size is set to 30, Style is set to Bold.

 

Following is a video I try to put together for better understanding. Try to watch it at full screen. That video displays problem only for 1/9 at the beginning. Problem I am having is with number 1 in that video. That exact problem maybe on number 9, or both 1 and 9 numbers and it maybe in a total different step of the operation. It may happen more than once, too. Interestingly forward slash never has a problem at all. Moreover, I do not always have that problem. It sometimes displays everything as it should be.

 

My guess, depending on internet speed some steps are completed fast. Sometimes pretty fast. When that happens, I get a very tiny text displayed on that Label. I did not have a chance to try reproducing that on a regular mobile phone. Once I find time, I am going to do that for sure.

 

I wonder if that is some kind of a known problem and if there is a way to fix it.

 

Thanks & regards,

Ertan

Share this post


Link to post

If I had to use Synchronize, I'd only use it to trigger an update in the UI.

TThread.Synchronize(nil, procedure begin RefreshTimer.Enabled := True end);

and perform the actual update in the timer event handler.

 

I have had so many bad side effects with Synchronize, that I simply prefer to not use it at all.

  • Like 1

Share this post


Link to post

My advice would be the same.
Stay away from TThread.Synchronize calls if possible.

Instead,  either call  TThread.Queue - that works a bit better.

or even better, have  a shared variable, in which the thread writes the current step.
In the main GUI thread have a timer, and read out that same variable and update the label there.
And don't forget to protect that shared variable with a critical section or similar.

  • Like 1

Share this post


Link to post
3 hours ago, Pawel Piotrowski said:

And don't forget to protect that shared variable with a critical section or similar.

... or use a variable type that does not require such protection because access is atomic, e.g. an integer on Windows (not sure about other platforms).

  • Like 1

Share this post


Link to post
4 hours ago, Pawel Piotrowski said:

And don't forget to protect that shared variable with a critical section or similar.

Do I still need to do such protection for reading only? Thread will be the only process which is writing into a string variable.

Share this post


Link to post
50 minutes ago, ertank said:

Do I still need to do such protection

Variables SizeOf Pointer or below is always atomic.

Atomic means access to a full variable in one read/write.

Share this post


Link to post
2 hours ago, ertank said:

Do I still need to do such protection for reading only? Thread will be the only process which is writing into a string variable. 

 

First things first, what you describe is not read only.
When  a thread writes and an other thread reads, then you need to protect the variable. Always.

OK, almost always. There are types that are guaranteed to have atomic read write operations (see comments above). So they are protected by the CPU. Anyway, you need to be aware of this, even if you do not need to protect the variable yourself. And you need to double check the architecture of the device your code will run on. I would say, all of them should have the same rules regarding atomic read write operations... but...

In case of strings. Protect them.
See here for a topic on why: http://codeverge.com/embarcadero.delphi.general/strings-thread-safety/1051533
    
itself is not.

 

  • Like 1

Share this post


Link to post
13 hours ago, FredS said:

Variables SizeOf Pointer or below is always atomic.

Atomic means access to a full variable in one read/write.

Are you sure about that? What about a packed record which starts with a byte followed by an integer? Is access to that integer atomic? On all platforms?

Share this post


Link to post
3 hours ago, dummzeuch said:

Are you sure about that? What about a packed record which starts with a byte followed by an integer? Is access to that integer atomic? On all platforms? 

 

Of course it wont be atomic!

 

There is a very very important rule besides the size. A variable must be aligned  by 32bit boundaries.

 

on x86systems , a 32-bit mov instruction is atomic if the memory operand is naturally aligned, but non-atomic otherwise. In other words, atomicity is only guaranteed when the 32-bit integer is located at an address which is an exact multiple of 4.
That rule is true on all modern x86, x64, Itanium, SPARC, ARM and PowerPC processors: plain 32-bit integer assignment is atomic as long as the target variable is naturally aligned.

 

Which the integer in your packed record will most likely not be.  - unless... it is part of an other record, that has 3 byte fields before your packed record 😉

So to assume atomicity, you need to know where your variable is placed in the memory...

 

Your record may be safe anyway - but by  luck not by design - because there is something like  a CPU cache line.
The CPU has some cache and that one is locked too, so the multiple cores can not access it simultaneously...
see here for more details, I'm not an expert on that:  http://delphitools.info/2011/11/30/fixing-tcriticalsection/

 

To sum up, use TInterlocked to access  simple types if you are not 100% sure they will be  naturally aligned.

  • Like 1

Share this post


Link to post
7 hours ago, dummzeuch said:

What about a packed record

OK, simple variables and lets add default aligned, but that was not the question.
 

As you can see with the TSrwLock declaration, the FIRST variable in a record is always properly aligned.

 

I use this during initialization:

//MMWIN:MEMBERSCOPY
unit _MM_Copy_Buffer_;

interface

type
  TAtomic = record
     /// <summary>
     ///   Assures {$A8} or {$A+} state
     /// </summary>
     /// <seealso href="https://stackoverflow.com/questions/829235/ifopt-a4">
     ///   {$IFOPT A4}?
     /// </seealso>
    class constructor Create;
  end;


implementation

 { TAtomic }

class constructor TAtomic.Create;
type
  TTestRec = record
    A: Byte;
    B: Int64;
  end;
begin
  // In the {$A8} or {$A+} state, fields in record types that are declared without the packed modifier and fields in class structures are aligned on quadword boundaries.
{$IF SIZEOF(TTestRec) <> 16}
  'App must be compiled in A8 mode'
{$IFEND}
end;


end.

 

Edited by FredS

Share this post


Link to post
On 11/23/2019 at 3:40 PM, Pawel Piotrowski said:

There is a very very important rule besides the size. A variable must be aligned  by 32bit boundaries.

Hm, what if a variable isn't aligned? It produces single mov instruction anyway. Or do you mean that CPU will have to do several instructions to modify that variable?

If that is true, seems no simple variable or member could be safely accessed without locks, at least until a code aligns it at runtime explicitly

Share this post


Link to post
2 minutes ago, Fr0sT.Brutal said:

Hm, what if a variable isn't aligned? It produces single mov instruction anyway. Or do you mean that CPU will have to do several instructions to modify that variable?

If that is true, seems no simple variable or member could be safely accessed without locks, at least until a code aligns it at runtime explicitly

Most variables are automatically aligned on a 32 bit boundary. That's the compiler default. So unless you explicitly make something not aligned, e.g. see my example above, or you access some data structure created by foreign code, there won't be any problem.

Share this post


Link to post

What about string variable pointing to "1/9" as value and that is not going to be 4 characters in total ever?

Share this post


Link to post
40 minutes ago, ertank said:

What about string variable pointing to "1/9" as value and that is not going to be 4 characters in total ever?

For a Delphi string, it's never threadsafe if you have one thread writing and one thread reading.
What is safe is the reference counting of strings.
Copy-On-Write of Delphi strings is not a threadsafe operation. if you need a multithreaded read/write access to the same string you generally should use some synchronization, otherwise you are potentially in trouble.

 

Example of what could happen without any lock.

String is being written: it should become bigger than it was, so new memory is allocated. But pointer is not yet modified, it points to old string.

At the same time reading thread got a pointer and began to read old string.

Context switched again to writing thread. It changed pointer, so now it is valid. Old string got refcount 0 and was immediately freed.

Context switch again: reading thread continues to process old string, but now it is access to deallocated memory which may easily result in access violation.

 

3 hours ago, Fr0sT.Brutal said:

Hm, what if a variable isn't aligned? It produces single mov instruction anyway. Or do you mean that CPU will have to do several instructions to modify that variable?

dummzeuch is correct. Usually, you do not need to worry, Delphi plays nice and takes care for you.

 

But if you want to know why non aligned memory access is not atomic, here is why:
 
The problem is not limited to CPU instructions.  In fact it has more to do with the data bus.
When you have a 32 bit wide data bus, a read from memory is aligned on that boundary.
 
So if you were to perform a 32 bit read from address 0x02, then two memory cycles are required, a read from address 0x00 to get two of the bytes and a read from 0x04 to get the other two bytes.

 

You see, the first read fetches all 4 bytes from the address   0x00, discards the first 2 bytes.
The second read fetches all 4 bytes from 0x04, and similar discards the second two bytes.
After that the remaining bytes are combined to give you your 32bit data that you requested.
 
This is not guaranteed to be atomic. A different CPU core could get its chance in between the above two reads to change the memory at address at  0x04, just before you read it.
 
This is why you can not assume atomicity with non aligned variables.

 

You might get lucky, because the CPU has multiple caches, and it might happen to be safe. But it is not guaranteed to be atomic.

 

On a similar note, aligned memory is twice as fast  to read/write, and this is why Delphi (and other compilers) align instructions and data in memory.

 

3 hours ago, dummzeuch said:

Most variables are automatically aligned on a 32 bit boundary. That's the compiler default. So unless you explicitly make something not aligned, e.g. see my example above, or you access some data structure created by foreign code, there won't be any problem.

 

I've build a small test, to see, how the following record will be aligned: Just for fun.
 

TmyNiceRecord = Record
    i1: Integer;
    i64: int64;
    b1, b2, b3: byte;
    w1, w2: word;
    b4: byte;
    i2: Integer;
    b5: byte;
    b6: byte;
    b7: byte;
  End;


 
  who can guess which fields are aligned properly? Which are guaranteed to be atomic?
  Here are the results:
 
 
 for win32 build, with delphi 10.3.2
 

Address*| Aligned | VarName | VarSize
0       | Yes     | i1      | 4
8       | Yes     | i64     | 8
16      | Yes     | b1      | 1
17      | No      | b2      | 1
18      | No      | b3      | 1
20      | Yes     | w1      | 2
22      | No      | w2      | 2
24      | Yes     | b4      | 1
28      | Yes     | i2      | 4
32      | Yes     | b5      | 1
33      | No      | b6      | 1
34      | No      | b7      | 1

 (* as an offset to the record itself)
 
For win64

Address | Aligned | VarName | VarSize
0       | Yes     | i1      | 4
8       | Yes     | i64     | 8
16      | Yes     | b1      | 1
17      | No      | b2      | 1
18      | No      | b3      | 1
20      | Yes     | w1      | 2
22      | No      | w2      | 2
24      | Yes     | b4      | 1
28      | Yes     | i2      | 4
32      | Yes     | b5      | 1
33      | No      | b6      | 1
34      | No      | b7      | 1

interesting, isn't it?

 

So, is delphi playing nice? Which fields can be assumed to be atomic?

The answer is: All of them.

Even b2 and b3. Yes, they do not start with a aligned address, but they do not cross the boundary either.

This means, the read/write is still atomic.

 

I hope that helps to better understand the topic.

MemoryAlignedOrNot.zip

  • Like 1
  • Thanks 1

Share this post


Link to post
14 minutes ago, Pawel Piotrowski said:

For a Delphi string, it's never threadsafe if you have one thread writing and one thread reading.

Was that a typo?

Share this post


Link to post
1 hour ago, Pawel Piotrowski said:

NOT thread Safe

 

Been on the back of my mind since I read Marco's book but I never had to deal with strings where a lock wasn't used.

However, if the main thread (mt) only reads and a worker thread (wt) only writes this should be fine.

 

The way I see this is that when the wt writes to a string variable if first makes a reference copy of the original. If the mt reads that string it gets the old value while its being written to.
Once the write lock is off the string var now points to the new string and when the mt tries to read it the next time around it gets a reference to that string.

 

A quick test is below, I used a Cursive Number method to make the string more complex for the test, I know a version of that is available here somewhere.
Unfortunately the version available does not reverse from Text. But you can simply replace these three calls with CurrToStr/StrToCurr.

 

Guess I  could let that run a bit longer and see if there are any collisions..

 

//MMWIN:MEMBERSCOPY
unit _MM_Copy_Buffer_;

interface

type
  THelperTests = class(TObject)
  public
    [Test]
    [TestCase('10k', '10000')]
    procedure TestStringThreadSafe(Iterations: Integer);
  end;


implementation

procedure THelperTests.TestStringThreadSafe(Iterations: Integer);
var
  c, LastReadC : Currency;
  s : string;
  HasTerminated : Boolean;
  i : integer;
const IncBy = 1000;
begin
//  Assert.IsTrue(TAtomic.IsNatural(c, SizeOf(c)), 'Currency will only work in x64');

  c := 1000.34;
  s := TCursiveNumbers.ToText(c);

  {- Reading }
  TThread.CreateAnonymousThread( procedure
    var rS : string; CurrentC  : Currency;
    begin
      LastReadC := c;
      rS := s;
      while not HasTerminated do
        if rS <> S then begin
          rS := S;
          CurrentC := TCursiveNumbers.ToCurrency(rS);
          Assert.AreEqual(LastReadC + IncBy, CurrentC, 'Oops');
          LastReadC := CurrentC;
        end;
    end).Start;

  for i := 0 to Iterations do begin
    c := c + IncBy;
    s := TCursiveNumbers.ToText(c);
    Sleep(1); // give the slice away
  end;

  HasTerminated := True;
  Sleep(2);
  Assert.AreEqual(c, LastReadC, 'Reading does not match writing');
  Log(S);
end;


end.

 

 

Edited by FredS
No reverse in https://en.delphipraxis.net/topic/1761-is-there-any-currency-to-words-routine-for-delphi/

Share this post


Link to post
12 minutes ago, Attila Kovacs said:

Does it changes the length of the string?

Yes, ends with "One Million Two Thousand and 34/100"

  • Thanks 1

Share this post


Link to post

@FredS What happens if you add     s := Copy(s, 1, Length(s) - 1) + Copy(s, Length(s), 1); after s := TCursiveNumbers.ToText(c); in the main loop?

Share this post


Link to post

I'm not sure if y

I'm not sure if your testcase not crashing is a proof that  strings are now thread safe.

remember how strings are designed:
http://www.marcocantu.com/epascal/English/ch07str.htm

 

so the string is just a pointer to the string content and before that you will find the length and the reference.

In order to increase the reference, the CPU needs to get the address of the content. Then decrease it, and only then has it access to the reference.
And only then can it safely increase the reference.
That are multiple read/write operations right there. This is not atomic.

 

Increase the number of threads that perform the writing and reading. Mayby then it will crash.


I've prepared a small test app myself.
It crashes almost instantanously. Try it out.
And even if it doesn't crash, it shows errors in the string length.
But if you enable the critical section, all is fine again.

 

 

Unit Unit1;

{ .$DEFINE UseCS }

Interface

Uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
{$IFDEF UseCS }
  syncObjs,
{$ENDIF}
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;

Type
  TForm2 = Class(TForm)
    Timer1: TTimer;
    StaticText1: TStaticText;
    Procedure FormCreate(Sender: TObject);
    Procedure Timer1Timer(Sender: TObject);
    Procedure FormDestroy(Sender: TObject);
  Private
    fErrorCounter: integer;
    fTerminated: boolean;
{$IFDEF UseCS}
    fCS: TCriticalSection;
{$ENDIF}
    Procedure asyncWrite;
    Procedure asyncRead;
  Public

  End;

Var
  Form2: TForm2;
  GlobalString: String;
  TempString: String;

Implementation

{$R *.dfm}


Procedure TForm2.asyncRead;
Var
  len: integer;
  x: integer;
  s: String;
Begin
  Repeat
{$IFDEF UseCS}
    fCS.Enter;
    Try
{$ENDIF}
      s := GlobalString;
      len := length(s);
{$IFDEF UseCS}
    Finally
      fCS.Leave;
    End;
{$ENDIF}
    For x := 0 To 99999 Do
    Begin
{$IFDEF UseCS}
      fCS.Enter;
      Try
{$ENDIF}
        If len <> length(s) Then
        Begin
            inc(fErrorCounter);
          break;
        End;
{$IFDEF UseCS}
      Finally
        fCS.Leave;
      End;
{$ENDIF}
    End;
  Until fTerminated;

End;

Procedure TForm2.asyncWrite;
Var
  x: integer;
Begin
  Repeat

{$IFDEF UseCS}
    fCS.Enter;
    Try
{$ENDIF}
      If random(2) = 0 Then
        GlobalString := ''
      Else
        GlobalString := StringOfChar('a', random(124));
{$IFDEF UseCS}
    Finally
      fCS.Leave;
    End;
{$ENDIF}
  Until fTerminated;
End;

Procedure TForm2.FormCreate(Sender: TObject);
Var
  x: integer;
Begin
  randomize;

{$IFDEF UseCS}
  fCS := TCriticalSection.create;
{$ENDIF}
  For x := 0 To 9 Do
  Begin
      TThread.CreateAnonymousThread(asyncWrite).Start;
    TThread.CreateAnonymousThread(asyncRead).Start;
  End;

End;

Procedure TForm2.FormDestroy(Sender: TObject);
Begin
  fTerminated := true;
End;

Procedure TForm2.Timer1Timer(Sender: TObject);
Begin
  StaticText1.Caption := IntToStr(self.fErrorCounter);
End;

End.

 

Edited by Pawel Piotrowski

Share this post


Link to post

@Pawel Piotrowski

 

Yes, but you launch 20 threads.
My point was about a single write single read thread.

 

3 hours ago, FredS said:

For a Delphi string, it's never threadsafe if you have one thread writing and one thread reading.


But regardless my test also fails when increased to 10 million with single steps..

 

 

  • Like 1

Share this post


Link to post
6 hours ago, FredS said:

My point was about a single write single read thread.

I know 🙂
But the number of threads doesn't matter to the correctness or falsehood of the assumption that strings are thread safe 😉
The high number of threads just helps to  increase the probability of bad things to happen 🙂
And we want bad things to happen while we write and test the code, and not after we ship the software.
When working with threads, one of the many problems is... it can work just fine... even, if there are problems in the code. At least for a while, on the development machine. The problems start rolling in, when you ship the software to some hundred customers. Then you will start to get strange bug reports from them. That is a very unlucky position to be in.

  • Like 1
  • Thanks 1

Share this post


Link to post
14 hours ago, dummzeuch said:

Most variables are automatically aligned on a 32 bit boundary. That's the compiler default. So unless you explicitly make something not aligned, e.g. see my example above, or you access some data structure created by foreign code, there won't be any problem.

It's a rare case for variables that are accessed from several threads to be independent. Usually they are fields of a structure/object/class so nothing could guarantee they're aligned without explicit measures. So you'll have to either ensure alignment (by using paddings, dummy fields, $A directives etc) or just accept that accessing variables of size more than 1 byte is probably not atomic.

10 hours ago, Pawel Piotrowski said:

The problem is not limited to CPU instructions.  In fact it has more to do with the data bus.
When you have a 32 bit wide data bus, a read from memory is aligned on that boundary.

Thanks for clarification, I missed the influence of data bus.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×