Search code examples
multithreadingf#immutabilityagent

F# synchronized access to list


Let's say I have a list of integers in the system:

let mutable data: int list = [1; 2; 3; 4; 5]

which is updated (by adding an element) by relatively few producers and consumed by lots of consumers.

Note: it's ok if consumers receive slightly outdated data.

What would be a proper way to synchronize access to this variable?

A) The safe approach would be to wrap this variable into agent and access it via serialized messages, we wouldn't even need mutable modifier here. But this approach seems to be sub-optimal, because it would unnecessarily make all read accesses synchronized.

B) AFAIK reference assignment is atomic in .NET, so between one publisher and all consumers a simple assignment should suffice:

Publisher: data <- newItem :: data

Consumer: data |> process

So just simple locking between publishers would be enough to wrap up this workflow?

let monitor = object()

Publisher: lock monitor (fun () -> data <- newItem::data)

Am I right with my assumptions? Which approach is preferred and would be more idiomatic for F#? Any better options?


Solution

  • You could use Interlocked.CompareExchange to handle publishing without explicitly locking:

    let mutable data = [1;2;3;4;5]
    
    let newValue = 0
    
    // To publish:
    let mutable tmp = data;
    while not(tmp.Equals(Interlocked.CompareExchange(&data, newValue::data, tmp))) do
        tmp <- data
    

    This would likely provide a small benefit if you have synchronized writers.

    If you decide you do want consumers to always have the latest data, a ReaderWriterLockSlim would allow you to completely synchronize the data without forcing reads to block on each call.

    That could look something like:

    let mutable data = [1;2;3;4;5]
    let rwl = ReaderWriterLockSlim()
    
    let newValue = 0
    
    // To publish:
    let publish newValue =
        rwl.EnterWriteLock()
        try
            data <- newValue :: data
        finally
            rwl.ExitWriteLock()
    
    // To read:
    let readCurrent =
        rwl.EnterReadLock()
        try
            data
        finally
            rwl.ExitReadLock()