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
 

Hoomla said:

Monader i F#
September 29, 2008 4:05 PM
 

rowandavies said:

I'm not sure that the reader monad really contributes much here - you don't gain much by "threading" the lock through, and the effect on the synax seems much worse than using "(fun () -> ...)"  (also, I find "<| fun () ->" to work quite well).

I'm teaching concurrency in F# at the moment, and I find a better way is just to define a function that generates the appropriate lock operations, like:

 let lockOps obj =
                 let wait () = ignore(Threading.Monitor.Wait obj)
                 let wakeWaiters () = Threading.Monitor.PulseAll obj
                 (lock obj, wait, wakeWaiters)

Here's an example of how to use this - no Monitor class, and no reproducing the locked object, and now no curly braces either!

let inc, dec =
   let count = ref 0
   let lockIt, wait, wakeWaiters = lockOps count
   let inc() = lockIt <| fun () ->
       count := !count + 1
   let dec() = lockIt <| fun () ->
       count := !count - 1
   inc,dec

Also, your last example isn't generally what you want - the enqueue and dequeue only work correctly if every use of them for a particular queue use the same lock.  This should be bound when the queue is created - having a function like move that takes a queue and then decides what lock to use is just asking for trouble.  If you want to parameterize with respect to the lock, just use a function parameter (probably before the q argument) and return appropriate functions using that lock:

let enqDeq lockObj (q : Queue<_>) x =
   let lockIt, wait, wakeWaiters = lockOps lockObj
   let enq = lockIt <| fun()->
        if q.Count = 0 then wakeWaiters()
        q.Enqueue(x)
   let deq = lockIt <| fun()->
       while q.Count = 0 do wait()
       q.Dequeue()
   enq,deq  

April 15, 2009 8:18 AM
 

Matthew Podwysocki said:

In the previous post , we talked a bit about the State Monad , what it is and how you could use it today
January 6, 2010 10:32 PM
 

Matthew Podwysocki's Blog said:

In the previous post , we talked a bit about the State Monad , what it is and how you could use it today
January 6, 2010 10:47 PM
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