Recent

Author Topic: The unbearable "rightness" of syncing  (Read 9531 times)

MarkMLl

  • Hero Member
  • *****
  • Posts: 8505
The unbearable "rightness" of syncing
« on: June 27, 2021, 01:31:56 pm »
If one has a single background thread as well as the normal foreground/main thread, how thorough does one have to be with ones use of Synchronize() when the background thread wants to interact with the GUI?

I think that everybody agrees that if, for example, one wants to append text to a TMemo indicating e.g. a change of background status one has to use Synchronize().

I think that probably everybody would agree that if a form has some sort of status flag, that a background thread can poll this safely, and that that might extend to a status function depending on what's in it. This is, obviously, subject to the normal race conditions etc. that have to be considered whenever multiple threads interact.

But what if the background thread wants to read a checkbox's Checked state, or consult the ItemIndex from a RadioGroup? Does Synchronize() have to be used in these cases? If the answer is "maybe", is there a robust way one can test the requirements of a particular field/function/property or does one have to inspect the implementation?

MarkMLl
MT+86 & Turbo Pascal v1 on CCP/M-86, multitasking with LAN & graphics in 128Kb.
Logitech, TopSpeed & FTL Modula-2 on bare metal (Z80, '286 protected mode).
Pet hate: people who boast about the size and sophistication of their computer.
GitHub repositories: https://github.com/MarkMLl?tab=repositories

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 12535
  • FPC developer.
Re: The unbearable "rightness" of syncing
« Reply #1 on: June 27, 2021, 01:38:25 pm »
Synchronize schedules a method (iow code to run) into the mainthread. So that code is safe to access the mainthread and calling thread.

That requires that the main loop regularly to poll the queue in the eventloop (checksynchronize).

Besides synchronize() there is also queue, which is the same, but does not lock the calling thread (which continues). Much more efficient, but requires a bit of extra synchronization (critical sections)

jamie

  • Hero Member
  • *****
  • Posts: 7310
Re: The unbearable "rightness" of syncing
« Reply #2 on: June 27, 2021, 02:44:55 pm »
Which is another reason why mechanisms like Application.ProcessMessages and QueueCallAsync should not be allowed during the middle of user code or especially the LCL.

 I guess there could be a nested loop counter employed that I don't know about but its bad practice otherwise.

 Bad regardless of that, one should be using critical sections for all threads when it comes to GUI stuff especially..

The only true wisdom is knowing you know nothing

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 11818
  • Debugger - SynEdit - and more
    • wiki
Re: The unbearable "rightness" of syncing
« Reply #3 on: June 27, 2021, 02:50:50 pm »
But what if the background thread wants to read a checkbox's Checked state, or consult the ItemIndex from a RadioGroup? Does Synchronize() have to be used in these cases? If the answer is "maybe", is there a robust way one can test the requirements of a particular field/function/property or does one have to inspect the implementation?

Yes....
Well, you may be able to single out case, where it is not needed. But, you have to verify yourself if it would be future safe (including automatic OS update).

The first question is: Would it be save, if the LCL already had that status.
The answer: I do not know.
     But also: How do you know the LCL has it?

Also, the OS may call the main thread with an event, and then it may not be save. (Maybe an object is recreated, and the thread gets the dangling pointer)?

Or the LCL, may have to call the OS to get the state. The OS may or may not allow calls from other threads....

MarkMLl

  • Hero Member
  • *****
  • Posts: 8505
Re: The unbearable "rightness" of syncing
« Reply #4 on: June 27, 2021, 09:56:52 pm »
But what if the background thread wants to read a checkbox's Checked state, or consult the ItemIndex from a RadioGroup? Does Synchronize() have to be used in these cases? If the answer is "maybe", is there a robust way one can test the requirements of a particular field/function/property or does one have to inspect the implementation?

Yes....
Well, you may be able to single out case, where it is not needed. But, you have to verify yourself if it would be future safe (including automatic OS update).

The first question is: Would it be save, if the LCL already had that status.
The answer: I do not know.
     But also: How do you know the LCL has it?

Also, the OS may call the main thread with an event, and then it may not be save. (Maybe an object is recreated, and the thread gets the dangling pointer)?

Or the LCL, may have to call the OS to get the state. The OS may or may not allow calls from other threads....

Thanks for that Martin. Would a good summary be that user-defined fields in a form (etc.) should be safe for background access (subject to normal race conditions etc.), but that LCL-defined fields/properties should be assumed to never be safe?

I don't /think/ I've ever had a background thread directly access an LCL field, and if I have I'm not aware of any inexplicable problems (although of course "one can never prove a negative")... with the obvious exception of Application.Terminated.

Hopefully the original question made it clear that I'd never dream of changing the state of an LCL object from a background thread, and for the avoidance of doubt I've explored the various alternatives extensively and am fully aware of the problems that a nested APM can cause.

MarkMLl
MT+86 & Turbo Pascal v1 on CCP/M-86, multitasking with LAN & graphics in 128Kb.
Logitech, TopSpeed & FTL Modula-2 on bare metal (Z80, '286 protected mode).
Pet hate: people who boast about the size and sophistication of their computer.
GitHub repositories: https://github.com/MarkMLl?tab=repositories

dbannon

  • Hero Member
  • *****
  • Posts: 3565
    • tomboy-ng, a rewrite of the classic Tomboy
Re: The unbearable "rightness" of syncing
« Reply #5 on: July 01, 2021, 11:43:20 am »
I would like to followup on this discussion before it gets too stale.

* Firstly, its very clear that a thread process must not write to any LCL 'item'. Thats easy to understand.
* Next, given what was said below, reading an LCL item is equally bad. A bit surprising but I accept it.
* But a local variable defined in user code ? Writing can be potentially risky, if you write and another process reads at about the same time, it may get one value or the other. Is that the problem ?
* And that same local variable, maybe we can read it from a thread ?  In a time period where its not changing ? That 'feels' safe enough ....

Ok, now, getting more interesting ...

What about some data that is hidden behind a function or property ?  Code needs to be executed to 'read' that data, without having taken steps to lock access to those functions, can we risk more than one process calling such a function (to read) them at (almost) the same time ?

Writing would certainly require some locking IMHO ?

David
Lazarus 3, Linux (and reluctantly Win10/11, OSX Monterey)
My Project - https://github.com/tomboy-notes/tomboy-ng and my github - https://github.com/davidbannon

MarkMLl

  • Hero Member
  • *****
  • Posts: 8505
Re: The unbearable "rightness" of syncing
« Reply #6 on: July 01, 2021, 12:13:15 pm »
What about some data that is hidden behind a function or property ?

That is the core of the problem: I don't think there's an orthodox way for a caller to distinguish between a variable/field (which can't have immediate knock-on effects, unless these have been set up by e.g. debug registers) and a property (which can have side effects, hence probably can't be safely read let alone written).

This puts me in mind of two things. The first is languages (e.g. C/C++ and Modula-2) which require that a call of a parameterless function has empty parentheses: if these have properties, are they decorated as variables or functions?

The second is a philosophy which predates OO hence properties, which insisted that it was desirable for the caller to not be able to distinguish between a variable and a function and that the implementer should have total freedom to change the implementation at whim. Frankly I think that's a load of cods, since very often somebody debugging a program needs to be able to determine what's about to happen by inspection.

In practical terms, an unorthodox way for a caller to determine that he's about to read a property which might have side-effects is by trying to resolve its address. But I don't know whether that is reliable, and I very much dislike the idea since addresses/pointers/references should not be fabricated casually and in principle there's always a risk that the object containing a field be moved in the middle of the test-then-read sequence.

MarkMLl
MT+86 & Turbo Pascal v1 on CCP/M-86, multitasking with LAN & graphics in 128Kb.
Logitech, TopSpeed & FTL Modula-2 on bare metal (Z80, '286 protected mode).
Pet hate: people who boast about the size and sophistication of their computer.
GitHub repositories: https://github.com/MarkMLl?tab=repositories

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 11818
  • Debugger - SynEdit - and more
    • wiki
Re: The unbearable "rightness" of syncing
« Reply #7 on: July 01, 2021, 12:37:59 pm »
* Next, given what was said below, reading an LCL item is equally bad. A bit surprising but I accept it.
As MarkMLI hinted, this depends.

If you can establish, that the LCL item is a field, or variable.
An if (in case of a field) you can establish that the lifetime of its object will not end during your access; as well as the reference to that object will not change during your access.

Then you can read that field or variable.

However, you must re-establish those pre-conditions every time you update the LCL, or any package it interacts with.

Also the data may be outdated.
If reading the data requires more than one access to memory (Point x,y => read 2 fields), the data may be corrupt.

You can not relay on any relation between that field/var and any other field/var.

I.e, ever if the LCL code says
Code: Pascal  [Select][+][-]
  1. Value := xyz;
  2. ValueIsValid := true;
"ValueIsValid" may become true before Value gets assigned. The CPU can change that order on its own account (google read/write barriers).

So overall you are on shaky ground.


Quote
* But a local variable defined in user code ? Writing can be potentially risky, if you write and another process reads at about the same time, it may get one value or the other. Is that the problem ?
* And that same local variable, maybe we can read it from a thread ?  In a time period where its not changing ? That 'feels' safe enough ....
In general, you can access your own data from any thread you want.
Ideally the other thread is aware of it. (definitely if you write to it, but in some cases for read access to)

See the "ValueIsValid" example above. This applies here to.

Also obviously
Code: Pascal  [Select][+][-]
  1. If obj <> nil then FreeAndNil(obj);
does not work, if 2 threads do attempt to run this.
They could both see "obj <> nil" true. (even without the CPU doing some optimizations)

Even a simple
Code: Pascal  [Select][+][-]
  1. GlobalCnt := GlobalCnt + 1;
does not work, because after ThreadA has read the value, but before it writes it back, ThreadB may also have read it. And ThreadB will then add one to the not yet updated value.
So overall only one of the 2 adds, will be in the final result.

For those things you can use the InterlockedExchange / Increment / ....


But, if you have ONE global variable, that can be read or written in a single mem access. And if only ONE thread writes to it.
Then other threads can safely read it.

Again, so long as they do not relay that any other date is up to date, based on the value that they read..
And so long, as an outdated result will be ok, and caught later.

Code: Pascal  [Select][+][-]
  1. MyThread.Terminated := True;
  2.  
Does exactly that. It sets a variable.

If the thread checks, and still gets  false, it runs one more iteration of whatever it does. And then it will see the True, and exit.



Quote
What about some data that is hidden behind a function or property ?  Code needs to be executed to 'read' that data, without having taken steps to lock access to those functions, can we risk more than one process calling such a function (to read) them at (almost) the same time ?

The code can execute in parallel.  Each thread has its own stack, and local vars.
But access to any none local data, falls under the same rules as above.



dbannon

  • Hero Member
  • *****
  • Posts: 3565
    • tomboy-ng, a rewrite of the classic Tomboy
Re: The unbearable "rightness" of syncing
« Reply #8 on: July 01, 2021, 01:09:56 pm »
Thanks Martin_fr (and Mark) !

That has really clarified things for me. Given the qualifiers, there is no way I will even look at LCL items.  Far too much could go wrong. And you have to think of the next person to read the code.

But easy access to my data is a real possibility. If a thread has its own stack and local vars, reading is safe ! Good.

I will link to this item from the multithreaded wiki page assuming no one minds.

Davo
Lazarus 3, Linux (and reluctantly Win10/11, OSX Monterey)
My Project - https://github.com/tomboy-notes/tomboy-ng and my github - https://github.com/davidbannon

PascalDragon

  • Hero Member
  • *****
  • Posts: 6195
  • Compiler Developer
Re: The unbearable "rightness" of syncing
« Reply #9 on: July 01, 2021, 01:30:49 pm »
Which is another reason why mechanisms like Application.ProcessMessages and QueueCallAsync should not be allowed during the middle of user code or especially the LCL.

There is no reason to prohibit ProcessMessages or QueueAsyncCall in user code: QueueAsyncCall simply enqueues a method call that will be executed upon the next time the LCL processes messages. And calling ProcessMessages is necessary when you do a longer operation inside the GUI thread while you still want to e.g. react to events or you want to update a progress bar.

* But a local variable defined in user code ? Writing can be potentially risky, if you write and another process reads at about the same time, it may get one value or the other. Is that the problem ?
* And that same local variable, maybe we can read it from a thread ?  In a time period where its not changing ? That 'feels' safe enough ....

And how do you know that it's "not changing"?

At best you use synchronization primitives like critical sections and at least memory barriers, cause on weakly ordered systems like ARM (both 32- and 64-bit) you might not get what you want otherwise.

dbannon

  • Hero Member
  • *****
  • Posts: 3565
    • tomboy-ng, a rewrite of the classic Tomboy
Re: The unbearable "rightness" of syncing
« Reply #10 on: July 01, 2021, 02:05:56 pm »
....
And how do you know that it's "not changing"?

Well, in a lot of cases its highly unlikely to change because you set it before starting the threads and don't allow a change until after they have all finished. But also keeping in mind Martin_Fr message that what is read should not be timing critical. So, the variable FinishNow starts out false, if its set to true the timing of when the thread sees it as true may not really matter, as long as it does, finally, get the right message.

Yeah ?

Davo

 
Lazarus 3, Linux (and reluctantly Win10/11, OSX Monterey)
My Project - https://github.com/tomboy-notes/tomboy-ng and my github - https://github.com/davidbannon

dseligo

  • Hero Member
  • *****
  • Posts: 1601
Re: The unbearable "rightness" of syncing
« Reply #11 on: July 01, 2021, 07:13:49 pm »
But, if you have ONE global variable, that can be read or written in a single mem access. And if only ONE thread writes to it.
Then other threads can safely read it.

Should't that say: that must be read or written in a single mem access?

MarkMLl

  • Hero Member
  • *****
  • Posts: 8505
Re: The unbearable "rightness" of syncing
« Reply #12 on: July 01, 2021, 08:15:21 pm »
There is no reason to prohibit ProcessMessages or QueueAsyncCall in user code: QueueAsyncCall simply enqueues a method call that will be executed upon the next time the LCL processes messages. And calling ProcessMessages is necessary when you do a longer operation inside the GUI thread while you still want to e.g. react to events or you want to update a progress bar.

Except for the case where (something called by) a shim invoked due to a Synchronize() has an APM in it: that can go recursive IME.

MarkMLl
MT+86 & Turbo Pascal v1 on CCP/M-86, multitasking with LAN & graphics in 128Kb.
Logitech, TopSpeed & FTL Modula-2 on bare metal (Z80, '286 protected mode).
Pet hate: people who boast about the size and sophistication of their computer.
GitHub repositories: https://github.com/MarkMLl?tab=repositories

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 11818
  • Debugger - SynEdit - and more
    • wiki
Re: The unbearable "rightness" of syncing
« Reply #13 on: July 01, 2021, 08:24:00 pm »
But, if you have ONE global variable, that can be read or written in a single mem access. And if only ONE thread writes to it.
Then other threads can safely read it.

Should't that say: that must be read or written in a single mem access?
Well, yes.

The implication was that if it can, then it would.
Of course you can write a longint, by writing each byte individually.

It also means, that it actually is written. If it is kept in a a register, then the other thread never sees it.

MarkMLl

  • Hero Member
  • *****
  • Posts: 8505
Re: The unbearable "rightness" of syncing
« Reply #14 on: July 01, 2021, 08:26:38 pm »
But, if you have ONE global variable, that can be read or written in a single mem access. And if only ONE thread writes to it.
Then other threads can safely read it.

Should't that say: that must be read or written in a single mem access?

I think Martin means "is of a type that can be read", so it can be interpreted as excluding strings, dynamic arrays, depending on the architecture possibly the larger numerics and so on.

Broadly speaking, I did try to make the point in my OP that non-GUI status flags (which by implication are simple types) embedded in a form could probably be read safely, but that everything had to be considered in the context of robust multithread coding practice. And I've mentioned Application.Terminated.

Having said which, I'd be happier if (pointers to) forms were exposed as immutable properties rather than as global variables.

MarkMLl
MT+86 & Turbo Pascal v1 on CCP/M-86, multitasking with LAN & graphics in 128Kb.
Logitech, TopSpeed & FTL Modula-2 on bare metal (Z80, '286 protected mode).
Pet hate: people who boast about the size and sophistication of their computer.
GitHub repositories: https://github.com/MarkMLl?tab=repositories

 

TinyPortal © 2005-2018