Delphi threading - what parts of the code need to be protected / synchronized?

Until now, I thought that any operation performed on a “common” object (common for several threads) should be protected by “synchronization”, no matter what. Apparently, I was mistaken - in the code that I am studying recently, there are many classes (thread-safe, as the author claims), and only one of them uses a critical section for almost every method.

How do I know which parts / methods of my code need to be protected with CriticalSection (or any other method) and which are not?

So far I have not stumbled upon any interesting explanation / comment for an article / blog, all google results:

a) examples of synchronization between a stream and a graphical interface. From simple progress to the most difficult, but the lesson is obvious: every time you access / modify a property of a GUI component, do it in Synchronize. But nothing more.

b) articles explaining critical sections, mutexes, etc. Just different approaches to protection / synchronization.

c) Examples of very simple thread safe classes (thread safe stack or list) - they all do the same - implement lock / unlock methods that introduce / leave a critical section and return the actual stack / list pointer when locking.

Now I'm looking for an explanation of which parts of the code should be protected .

may be in the form of code;) but please do not provide me another "use of Synchronize to update progressbar" ...;)

Thank you!

+6
multithreading delphi
source share
5 answers

You ask specific answers to a very general question.

Basically, in addition to user interface operations, you should protect every access to shared memory / resources to avoid two potentially competing threads:

  • read inconsistent memory
  • write memory at the same time
  • try to use the same resource simultaneously with multiple threads ... until the resource is thread safe.

As a rule, I believe that any other workflow is safe, including operations that provide access to non-shared memory or non-shared objects.

For example, consider this object:

type TThrdExample = class private FValue: Integer; public procedure Inc; procedure Dec; function Value: Integer; procedure ThreadInc; procedure ThreadDec; function ThreadValue: Integer; end; ThreadVar ThreadValue: Integer; 

Inc, Dec, and Value are methods that work on the FValue field. These methods are not thread safe until you protect them with some kind of synchronization mechanism. It could be MultipleReaderExclusiveWriterSinchronizer for the Value function and CriticalSection for Inc and Dec.

The ThreadInc and ThreadDec methods work on the ThreadValue variable, which is defined as ThreadVar, so I consider ThreadSafe because the memory they access is not shared between threads ... each call from another thread will access a different memory address.

If you know that by design a class should be used only in one thread or inside other synchronization mechanisms, you can consider this thread safe by design.

If you want more specific answers, I suggest you try a more specific question.

Sincerely.

EDIT: Perhaps someone will say that integer fields are a bad example, because you can consider whole atom operations on Intel / Windows, so there is no need to protect it ... but I hope you get the idea.

+5
source share

You misunderstood the TThread.Synchronize method.

The TThread.Synchronize and TThread.Queue methods execute protected code in the context of the main (GUI) thread. That's why you should use Syncronize or Queue to update GUI controls (such as progressbar) - usually only the main thread should access the GUI controls.

The critical sections are different - the protected code is executed in the context of the thread that acquired the critical section, and no other thread is allowed to acquire the critical section until its previous thread is released.

+2
source share

You use a critical section if you need to atomically update a specific set of objects. This means that they must be constantly updated or not updated at all. They should never be available in transition.

For example, with a simple read / write of integers, this is not so. The operation of reading an integer, as well as the operation of writing it, are already atomic: you cannot read an integer in the middle of the processor writing it, half updated. This is either the old value or the new value always.

But if you want to increase the number atomically, you do not have one, but three operations that you need to perform immediately: read the old value in the processor cache, increase it and write it to memory. Each operation is atomic, but three of them are not.

One thread can read the old value (say 200), increase it by 5 in the cache, and at the same time, another thread can also read the value (another 200). Then the first stream writes back 205, while the second stream increases its cached value from 200 to 203 and writes back 203, rewriting 205. The result of two increments (+5 and +3) should be 208, but this is 203 due to not - -atomic operations.

So you use critical sections when:

  • A variable, a set of variables, or any resource is used from several threads and needs to be updated atomically.
  • It is not atomic in itself (for example, calling a function that is protected by a critical sector inside the function body is already an atomic operation)
+2
source share
+1
source share

If you use messaging to exchange data between threads, you can basically ignore synchronization primitives completely, because each thread only accesses its internal structures and the messages themselves. In essence, this is a much simpler and more scalable architecture than using synchronization primitives.

0
source share

All Articles