Using computation expressions to control monitor locks
C# has the lock statement to support the use of .NET monitor synchronization.
lock(this) { x--; }
This curly-brace delimited block of atomic statements is easily identified in code. However having to call Wait or Pulse inside the block is less appealing because this previously hidden Monitor class now comes to your attention and you have to make sure you wait/pulse on the same object that was locked.
lock(this) { x--; if(x==0) Monitor.Pulse(this); }
F# has the lock function which is basically a higher-order function version of C#'s lock statement. It has the same un-niceness with wait/pulse, plus you have to construct the body of the block into a function, and to me braces {…} look better than the verbose (fun () -> …).
lock this (fun () -> decr x; if x=0 then Monitor.Pulse(this))
We can solve all these issues and more with computation expressions in F#. We start by defining the Lock monad, which is actually just an instance of the reader monad. A value of type Lock<'a> is a computation that will execute under the context of a lock and return a result of type 'a. The type of the Lock monad is a function that accepts the locked object and returns the result of the computation. The following return and bind functions are the standard implementation of the reader monad.
type Lock<'a> = Lock of (obj -> 'a)
let apply (Lock f) lock = f lock
let mreturn x = Lock (fun lock -> x)
let bind m f = Lock (fun lock -> apply (f (apply m lock)) lock)
Next we need a function to run a Lock computation. This function takes as arguments an object to lock and the computation to run. It acquires a lock on the object, runs the computation and then releases the lock.
(* val run : 'a -> Lock<'b> -> 'b *)
let run lock m =
let lock = box lock
Monitor.Enter(lock)
try apply m lock
finally Monitor.Exit(lock)
Next we'll define some monadic actions for wait and pulse.
(* val wait : Lock<unit> *)
let wait = Lock (fun lock -> Monitor.Wait(lock) |> ignore)
(* val tryWait : int -> Lock<bool> *)
let tryWait (timeout : int) = Lock (fun lock -> Monitor.Wait(lock, timeout))
(* val pulse : Lock<unit> *)
let pulse = Lock (fun lock -> Monitor.Pulse(lock))
(* val pulseAll : Lock<unit> *)
let pulseAll = Lock (fun lock -> Monitor.PulseAll(lock))
Finally we need a builder class to enable computation expression syntax on the Lock type. This code is omitted here but can be downloaded in the full example.
Now at last we can write lock code in F# with curly braces, without seeing the Monitor class, and without reproducing the locked object to call wait and pulse.
lock { do decr x
if x = 0 then return! pulse } |> run this
Or a more complex example
lock { while ready.Count = 0 && !pending > 0 do return! wait
if ready.Count > 0
then return Some (ready.Dequeue())
else return None } |> run myLock
You can even have an alternate run function that takes a timeout on acquiring the lock.
(* val tryRun : 'a -> int -> Lock<'b> -> 'b option *)
let tryRun lock (timeout : int) m =
let lock = box lock
if Monitor.TryEnter(lock, timeout)
then try Some (apply m lock)
finally Monitor.Exit(lock)
else None
Additionally the Lock monad can be used to make synchronized code more composable. Say you have method that always needs to execute under a lock but either you don't know which lock or the lock needs to be entered at an outer scope. You can write this method so that it returns a Lock computation, which can be composed with other Lock computations. Consider functions to synchronously access items in a System.Collections.Generic.Queue.
(* val enqueue : Queue<’a> -> ‘a -> Lock<unit> *)
let enqueue (q : Queue<_>) x =
lock { if q.Count = 0 then return! pulseAll
do q.Enqueue(x) }
(* val dequeue : Queue<’a> -> Lock<’a> *)
let dequeue (q : Queue<_>) =
lock { while q.Count = 0 do return! wait
return q.Dequeue() }
Because these functions return a Lock<_> value the caller can never forget to acquire the lock and this will be enforced by the type system (of course the programmer can still acquire the wrong lock). Secondly these functions can naturally call pulse and wait without knowing the object the lock is acquired on.
With these functions you can easily write code that atomically moves an item between queues and returns the item.
let move q1 q2 =
lock { let! x = dequeue q1
do! enqueue q2 x
return x } |> run myLock
So if you have to use locks because you don't like STM or its performance isn't good enough or for legacy reasons, the lock monad could improve readability and abstraction.