In order to convince you that a readers-writer lock is not a stupid idea, I should finally show some numbers. In this article I'll present a minimalistic (but still real-life) example which allows us to compare different locking solutions.
All code from this article is available in project rwLock at https://github.com/gabr42/examples/tree/master/Reader-writer%20lock
To demonstrate the difference between various locking mechanisms, the demonstration program implements a very basic publish-subscribe pattern. The TPubSub class in the PublishSubscribe unit functions as a message hub with methods for handling subscribers (Subscribe, Unsubscribe) and publishers (Notify).
In principles, a publish-subscribe pattern is trivial to make - unless you want to support multithreaded programs. In the simples form you only need a list of subscribers (FSubscribers) and few simple functions.
To register a subscriber, the code just needs to add a callback to the subscriber list:
procedure TPubSub.Subscribe(callback: TCallback); begin FSubscribers.Add(callback); end;
De-registration is equally simple:
procedure TPubSub.Unsubscribe(callback: TCallback); begin FSubscribers.Remove(callback); end;
To send notifications to all interested parties the code just walks over the subscriber list and calls all callbacks:
procedure TPubSub.Notify(value: integer); begin for var subscriber in FSubscribers do subscriber(value); end;
This, as expected, gets complicated when one wants to use the message hub TPubSub in a multithreaded program. For example, while one thread is walking the list in TPubSub.Notify another may call TPubSub.Unsubscribe which would remove one element from the list and break the iteration.
[Don't believe me? The demonstration program supports locking mechanism "none" which makes it run without any locking. Run it and you'll observe a nice access violation crash.]
To fix the code in a multithreaded environment, we must introduce some form of locking. The simplest way is to add a critical section, for example by using Delphi's TMonitor to lock the entire TPubSub object:
procedure TPubSub.Subscribe(callback: TCallback); begin MonitorEnter(Self); FSubscribers.Add(callback); MonitorExit(Self); end; procedure TPubSub.Unsubscribe(callback: TCallback); begin MonitorEnter(Self); FSubscribers.Remove(callback); MonitorExit(Self); end; procedure TPubSub.Notify(value: integer); begin MonitorEnter(Self); for var subscriber in FSubscribers do subscriber(value); MonitorExit(Self); end;
In some situations this may be enough. A TMonitor implementation is quite fast, after all. There is, however, one scenario where this implementation can be drastically sped up.
When two threads in parallel call the Notify method above, one of them will have to wait until the other finishes although there is actually no reason for that. Both Notify calls could run in parallel without any problem. We just have to protect them against Subscribe and Unsubscribe.
Enter readers-writer lock
So why can we run two Notify methods in parallel but not a Notify and a Subscribe? The question we have to ask ourselves is - how does the code access the shared entity FSubscribers?
The Notify method reads from the shared data. It does not change the data and can therefore be executed multiple times in parallel.
Both Subscribe and Unsubscribe, however, write to the shared data. They change the data and two such operations cannot run in parallel.
And, as we have seen in the introduction, one cannot run one reader and one writer in parallel as the writer may break the reader.
This is a typical situation where a readers-writer lock can be used. When the data is modified, such lock would be acquired with an exclusive (write) access. When we are merely reading from the data, the lock would be acquired with a shared (read) access.
With the new TLightweightMREW the modifications are trivial. As the lock is implemented with a record, we don't need to manage its lifecyle; just declare it inside the TPubSub class:
TPubSub = class FLockLight: TLightweightMREW; ... end; procedure TPubSub.Subscribe(callback: TCallback); begin FLockLight.BeginWrite; FSubscribers.Add(callback); FLockLight.EndWrite; end; procedure TPubSub.Unsubscribe(callback: TCallback); begin FLockLight.BeginWrite; FSubscribers.Remove(callback); FLockLight.EndWrite; end; procedure TPubSub.Notify(value: integer); begin FLockLight.BeginRead; for var subscriber in FSubscribers do subscriber(value); FLockLight.EndRead; end;
Great article Primoz!
ReplyDelete