DDGG
DDGG

Reputation: 1241

Is it necessary to do Multi-thread protection for a Boolean property in Delphi?

I found a Delphi library named EventBus and I think it will be very useful, since the Observer is my favorite design pattern.

In the process of learning its source code, I found a piece of code that may be due to multithreading security considerations, which is in the following (property Active's getter and setter methods).

TSubscription = class(TObject)
private
  FActive: Boolean;
  procedure SetActive(const Value: Boolean);
  function GetActive: Boolean;
  // ... other members
public
  constructor Create(ASubscriber: TObject;
    ASubscriberMethod: TSubscriberMethod);
  destructor Destroy; override;
  property Active: Boolean read GetActive write SetActive;
  // ... other methods
end;

function TSubscription.GetActive: Boolean;
begin
  TMonitor.Enter(self);
  try
    Result := FActive;
  finally
    TMonitor.exit(self);
  end;
end;

procedure TSubscription.SetActive(const Value: Boolean);
begin
  TMonitor.Enter(self);
  try
    FActive := Value;
  finally
    TMonitor.exit(self);
  end;
end;

Could you please tell me the lock protection for FActive is whether or not necessary and why?

Upvotes: 3

Views: 799

Answers (1)

Disillusioned
Disillusioned

Reputation: 14832

Summary

Let me start by making this point as clear as possible: Do not attempt to distill multi-threaded development into a set of "simple" rules. It is essential to understand how the data is shared in order to evaluate which of the available concurrency protection techniques would be correct for a particular situation.

The code you have presented suggests the original authors had only a superficial understanding of multi-threaded development. So it serves as a lesson in what not to do.

  • First, locking the Boolean for read/write access in that way serves no purpose at all. I.e. each read or write is already atomic.
  • Furthermore, in cases where the property does need protection for concurrent access: it fails abysmally to provide any protection at all.

The net effect is redundant ineffective code that can trigger pointless wait states.


Thread-safety

In order to evaluate 'thread-safety', the following concepts should be understood:

  • If 2 threads 'race' for the opportunity to access a shared memory location, one will be first, and the other second. In the absence of other factors, you have no control over which thread would 'start' its access first.
  • Your only control is to block the 'second' thread from concurrent access if the 'first' thread hasn't finished its critical work.
    • The word "critical" has loaded meaning and may take some effort to fully understand. Take note of the explanation later about why a Boolean variable might need protection.
    • Critical work refers to all the processing required for the operation on the shared data to be deemed complete.
    • It's related to concepts of atomic operations or transactional integrity.
    • The 'second' thread could either be made to wait for the 'first' thread to finish or to skip its operation altogether.
  • Note that if the shared memory is accessed concurrently by both threads, then there's the possibility of inconsistent behaviour based on the exact ordering of the internal sub-steps of each thread's processing.

This is the fundamental risk and area of concern when thinking about thread-safety. It is the base principle from which other principles are derived.


'Simple' reads and writes are (usually) atomic

No concurrent operations can interfere with the reading/writing of a single byte of data. You will always either get the value in its entirety or replace the value in its entirety.

This concept extends to multiple bytes up to the machine architecture bit size; but does have a caveat, known as tearing.

  • When a memory address is not aligned on the bit size, then there's the possibility of the bytes spanning the end of one aligned location into the beginning of the next aligned location.
  • This means that reading/writing the bytes may take 2 operations at the machine level.
  • As a result 2 concurrent threads could interleave their sub-steps resulting in invalid/incorrect values being read. E.g.
    • Suppose one thread writes $ffff over an existing value of $0000 while another reads.
    • "Valid" reads would return either $0000 or $ffff depending on which thread is 'first'.
    • If the sub-steps run concurrently, then the reading thread could return invalid values of $ff00 or $00ff.
  • (Note that some platforms might still guarantee atomicity in this situation, but I don't have the knowledge to comment in detail on this.)

To reiterate: single byte values (including Boolean) cannot span aligned memory locations. So they're not subject to the tearing issue above. And this is why the code in the question that attempts to protect the Boolean is completely pointless.


When protection is needed

Although reads and writes in isolation are atomic, it's important to note that when a value is read and impacts a write decision, then this cannot be assumed to be thread-safe. This is best explained by way of a simple example.

  • Suppose 2 threads invert a shared boolean value: FBool := not FBool;
  • 2 threads means this happens twice and once both threads have finished, the boolean should end up having its starting value. However, each is a multi-step operation:
    • Read FBool into a location local to the thread (either stack or register).
    • Invert the value.
    • Write the inverted value back to the shared location.
  • If there's no thread-safety mechanism employed then the sub-steps can run concurrently. And it's possible that both threads:
    • Read FBool; both getting the starting value.
    • Both threads invert their local copies.
    • Both threads write the same inverted value to the shared location.
    • And the end result is that the value is inverted when it should have been reverted to its starting value.

Basically the critical work is clearly more than simply reading or writing the value. To properly protect the boolean value in this situation, the protection must start before the read, and end after the write.

The important lesson to take away from this is that thread-safety requires understanding how the data is shared. It's not feasible to produce an arbitrary generic safety mechanism without this understanding.

And this is why any such attempt as in the EventBus code in the question is almost certainly doomed to be deficient (or even an outright failure).

Upvotes: 9

Related Questions