Reputation: 5359
I'm trying to do an F# async computation that calls an C# callback when ready. The code is the following:
type Worker() =
let locker = obj()
let computedValue = ref None
let started = ref false
let completed = Event<_>()
let doNothing() = ()
member x.Compute(callBack:Action<_>) =
let workAlreadyStarted, action =
lock locker (fun () ->
match !computedValue with
| Some value ->
true, (fun () -> callBack.Invoke value)
| None ->
completed.Publish.Add callBack.Invoke
if !started then
true, doNothing
else
started := true
false, doNothing)
action()
if not workAlreadyStartedthen
async {
// heavy computation to calc result
let result = "result"
lock locker (fun () ->
computedValue := Some result
completed.Trigger result)
} |> Async.Start
But there's a problem, I want to trigger the completed event outside the lock, but I want to make sure that the triggering is thread safe (Actually, in this small example I could just trigger the event outside the lock as I know no one else will subscribe to it, but that's not always the case).
In C# events this is very easy to accomplish:
object locker = new object();
event Action<string> MyEvent;
void Raise()
{
Action<string> myEventCache;
lock (locker)
{
myEventCache = MyEvent;
}
if (myEventCache != null)
{
myEventCache("result");
}
}
How can I do the equivalent with F# events, freezing the list of subscribers inside the lock but invoking it outside the lock?
Upvotes: 3
Views: 386
Reputation: 47904
This isn't as straightforward in F# because Event<_>
doesn't expose its subscriber list, which is mutated by Add
/Remove
.
You can avoid this mutation by creating a new event for each handler.
let mutable completed = Event<_>()
//...
let ev = Event<_>()
let iev = ev.Publish
iev.Add(completed.Trigger)
iev.Add(callBack.Invoke)
completed <- ev
//...
let ev = lock locker <| fun () ->
computedValue := Some result
completed
ev.Trigger(result)
Upvotes: 1