Tuesday, November 29, 2011

Per-object locking

I was always holding the opinion that locks should be as granular as possible. Putting many small locks around many unrelated pieces of code is better then using one giant lock for everything.

To make this simpler, OmniThreadLibrary includes a very useful record called TOmniCS which allows you to do locking without doing any upfront initialization. Just declare it and you’re ready to go. [You can read more about it in my previous blog post.]

Locking can be as simple as this:

uses
GpLists,
OtlSync;

procedure ProcessList(const intf: IGpIntegerList);
begin
//...
end;

var
lock: TOmniCS;
intf: IGpIntegerList;

procedure Test1;
begin
intf := TGpIntegerList.Create;
//...
lock.Acquire;
try
ProcessList(intf);
finally lock.Release; end;
end;

(Yes, this is a stupid example, written just to demonstrate the use of TOmniCS.)

Another nice point is that OtlSync doesn’t pull in any other OmniThreadLibrary unit and doesn’t depend on being used from the OmniThreadLibrary infrastructure. You can use it with any threading model (TThread, AsyncCalls, Windows threads …) and it will just work.

Today, life got even better. You can now use locking by declaring only one variable which combines locking and data storage in one record.

var
lockedIntf: Locked<IGpIntegerList>;

procedure Test2;
begin
lockedIntf := TGpIntegerList.CreateInterface;
//...
lockedIntf.Acquire;
try
ProcessList(lockedIntf);
finally lockedIntf.Release; end;
end;

Locked<T> is again declared in the OtlSync unit. You can use it to lock access around practically everything including built-in simple types.

  Locked<T> = record
constructor Create(const value: T);
class operator Implicit(const value: Locked<T>): T;
class operator Implicit(const value: T): Locked<T>;
procedure Acquire;
procedure Release;
property Value: T read GetValue;
end;

As it declares Implicit operators to and from the wrapped type, it is very simple to use (as you can see in the example above). For the cases where implicit conversion doesn’t work, you can still access the wrapped object through the Value property. For example, you’d have to do it if you wrap an object instance to release the object.

procedure ProcessObjList(obj: TGpIntegerList);
begin
//...
end;

var
lockedObj: Locked<TGpIntegerList>;

procedure Test3;
begin
lockedObj := TGpIntegerList.Create;
try
//...
lockedObj.Acquire;
try
ProcessObjList(lockedObj);
finally lockedObj.Release; end;
//...
finally lockedObj.Value.Free; end;
end;

I’m pretty sure somebody will ask me: “Why not just use TMonitor?”

var
obj: TGpIntegerList;

procedure Test4;
begin
obj := TGpIntegerList.Create;
try
//...
System.TMonitor.Enter(obj);
try
ProcessObjList(obj);
finally System.TMonitor.Exit(obj); end;
//...
finally FreeAndNil(obj); end;
end;

There are plenty of reasons for that.

  • TMonitor was buggy since its inception (although I believe that Enter and Exit may be stable enough for release code) and I don’t like to use it.
  • Using TMonitor doesn’t convey your intentions. Any code anywhere can use TMonitor.Enter to lock any object and you won’t know it just by looking at variable/field declarations. Using Locked<T>, however, explicitly declares your intent.
  • TMonitor.Enter/Exit doesn’t work with interfaces, records and primitive types. Locked <T> does.
var
a1: Locked<integer>;

procedure Test5;
begin
a1 := 42;
a1.Acquire;
try
a1 := a1.Value * 2;
finally a1.Release; end;
end;

4 comments:

  1. Great thanks to Barry Kelly for the idea on initializing generic smart record through the Implicit operator. (http://blog.barrkel.com/2008/09/smart-pointers-in-delphi.html)

    ReplyDelete
  2. >>TMonitor was buggy since its inception
    Is it still buggy? Does Embarcadero know about this bugs? Were there any changes in TMontor's code since its inception?
    I'm very annoyed by RTL bugs because I start to doubt about every strange behaviour, is it my bug, or is it Delphi's

    ReplyDelete
  3. @Anton: Lots of bugs were fixed in XE2, but I remember reading about some problems even in this release somewhere and I can't find the source now. In XE (and before) it was buggy up to being useless (except maybe for basic Enter/Exit usage).

    ReplyDelete
  4. Anonymous16:26

    XE2 issue mentioned here:
    http://stackoverflow.com/questions/4856306/tthreadedqueue-not-capable-of-multiple-consumers

    ReplyDelete