Reputation: 10482
I am experimenting a bit with F# and I wrote a class that listens incoming UDP packets, prints them, and continues to listen.
I have four different implementations that all accomplish this.
type UdpListener(endpoint:IPEndPoint) =
let client = new UdpClient(endpoint)
let endpoint = endpoint
let rec listenAsync1() =
async {
let! res = client.ReceiveAsync() |> Async.AwaitTask
res.Buffer |> printfn "%A"
return! listenAsync1()
}
let rec listenAsync2() =
async {
let res = client.Receive(ref endpoint)
res |> printfn "%A"
do! listenAsync2()
}
let rec listenAsync3() =
async {
let res = client.Receive(ref endpoint)
res |> printfn "%A"
listenAsync3() |> Async.Start
}
let rec listenAsync4() =
async {
while true do
let res = client.Receive(ref endpoint)
res |> printfn "%A"
}
member this.Start() =
listenAsync1() |> Async.Start
listenAsync1
attempts to leverage the awaitable returned by client.ReceiveAsync()
and re-listens using recursion. This approach feels most functional to me.
However, the async computation expression will actually run the code in the async
block on a TP thread, so is it really necessary to using the Task based client.ReceiveAsync()
?
listenAsync2
accomplishes the same result as listenAsync1
by using a blocking call on a TP thread.
listenAsync3
uses a slightly different way of recursively kicking off the listener again.
listenAsync4
uses a loop. It states intent pretty clearly, but is not really that pretty.
Is there an advantage of ever using Task based async in F#? It seems superfluous when wrapped inside an async computation expression which seems similar to Task.Run(..)
in C#.
Which of the methods (if any!) is generally accept as best practice, and why? (Perhaps they can be ranked?)
Upvotes: 3
Views: 291
Reputation: 80915
When you use blocking calls, you occupy the thread. That is, the thread is "busy" with your call and cannot be allocated for other work.
When you await a task, on the other hand, you relinquish control completely and the thread is free to go do other stuff.
In practice, this distinction will manifest in your application not being able to scale to a large number of threads. That is, if you make two calls at once, you've occupied two threads. Four calls - four threads. And so on. One could argue that this sorta defeats the whole idea of being "async".
On the other hand, if you make multiple calls simultaneously using awaitable tasks, your application may consume no threads at all (while the calls are in flight)!
Because of this, all three blocking versions are significantly inferior. Use the first one.
Update: you may also want to look at this answer.
Upvotes: 4