hubFS: THE place for F#

. . . are you on The Hub?
Welcome to hubFS: THE place for F# Sign in | Join | Help
in Search

Hell Is Other Languages

Using computation expressions to control monitor locks

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.

Published Sunday, February 17, 2008 11:40 PM by gneverov
Attachment(s): lock.zip

Comments

 

Jason Haley said:

February 17, 2008 8:56 AM
 

Hell Is Other Languages said:

Introduction
F# has the async computation expression for writing parallel programs. Async achieves concurrency...
August 3, 2008 2:54 AM
Anonymous comments are disabled

This Blog

Post Calendar

<February 2008>
SuMoTuWeThFrSa
272829303112
3456789
10111213141516
17181920212223
2425262728291
2345678

Syndication

Powered by Community Server, by Telligent Systems