I kind of know the syntax of asynchronous programming in F#. E.g.
let downloadUrl(url:string) = async { let req = HttpWebRequest.Create(url) // Run operation asynchronously let! resp = req.AsyncGetResponse() let stream = resp.GetResponseStream() // Dispose 'StreamReader' when completed use reader = new StreamReader(stream) // Run asynchronously and then return the result return! reader.AsyncReadToEnd() }
In F# expert book (and many other sources), they say like
let! var = expr simply means "perform the asynchronous operation expr and bind the result to var when the operation completes. Then continue by executing the rest of the computation body"
I also know that a new thread is created when performing async operation. My original understanding was that there are two parallel threads after the async operation, one doing I/O and one continuing to execute the async body at the same time.
But in this example, I am confused at
let! resp = req.AsyncGetResponse() let stream = resp.GetResponseStream()
What happens if resp
has not started yet and the thread in the async body wants to GetResponseStream
? Is this a possible error?
So maybe my original understanding was wrong. The quoted sentences in the F# expert book actually means that "creating a new thread, hang the current thread up, when the new thread finishes, wake up the body thread and continue", but in this case I don't see we could save any time.
In the original understanding, the time is saved when there are several independent IO operations in one async block so that they could be done at the same time without intervention with each other. But here, if I don't get the response, I cannot create the stream; only I have stream, I can start reading the stream. Where's the time gained?
The "async" in this example is not about concurrency or saving time, rather it's about providing a good programming model without blocking (read: wasting) threads.
If using other programming languages, typically you have two choices:
You can block, typically by calling synchronous methods. The disadvantage is that the thread is consumed and doing no useful work while it waits for the disk or network I/O or what have you. The advantage is it the code simple (normal code).
You can use callbacks to call asynchronously and get notifications when operations complete. The advantage is you don't block threads (these threads can be returned e.g. to the ThreadPool and a new ThreadPool thread will be used when the operation completes to call you back). The disadvantage is that a simple block of code gets divided up into a bunch of callback methods or lambdas, and it quickly becomes very complicated to maintain state/control-flow/exception-handling across the callbacks.
So you're between a rock and a hard place; you either give up the simple programming model or you waste threads.
The F# model gives the best of both worlds; you don't block threads, but you keep the straightforward programming model. Constructs like let!
enable you to 'thread-hop' in the middle of an async block, so in code like
Blah1() let! x = AsyncOp() Blah2()
Blah1
may run on, say, ThreadPool thread #13, but then AsyncOp will release that thread back to the ThreadPool. Later when the AsyncOp completes, the rest of the code will start back up on an available thread (maybe, say, ThreadPool thread #20) which binds x
to the result and then runs Blah2
. In trivial client apps this rarely matters (except when ensuring you don't block the UI thread), but in server apps that do I/O (where threads are often a precious resource - threads are expensive and you can't waste them by blocking) non-blocking I/O is often the only way to make an application scale. F# enables you to write non-blocking I/O without having the program degrade into a mass of spaghetti-code callbacks.
See also
Best practices to parallelize using async workflow
How to do chained callbacks in F#?
http://cs.hubfs.net/forums/thread/8262.aspx
I think the most important thing to understand about asynchronous workflows is that they are sequential in the same way as ordinary code written in F# (or C#, for that matter) is sequential. You have some let
bindings that evaluate in the usual order and some expressions (that may have side-effects). In fact, asynchronous workflows often look more like imperative code.
The second important aspect of asynchronous workflows is that they are non-blocking. This means that you can have operations that are executed in some non-standard way and do not block the thread while executing. (In general, let!
in F# computation expressions always signals that there is some non-standard behavior - it may be possibility to fail without producing result in the Maybe monad, or it may be non-blocking execution for asynchronous workflows).
Technically speaking, non-blocking execution is implemented by registering some callback that will be triggered when the operation completes. Relatively simple example is an asynchronous workflow that waits some specified time - this can be implemented using Timer
without blocking any threads (Example from chapter 13 of my book, source is available here):
// Primitive that delays the workflow let Sleep(time) = // 'FromContinuations' is the basic primitive for creating workflows Async.FromContinuations(fun (cont, econt, ccont) -> // This code is called when workflow (this operation) is executed let tmr = new System.Timers.Timer(time, AutoReset=false) tmr.Elapsed.Add(fun _ -> // Run the rest of the computation cont()) tmr.Start() )
There are also several ways to use F# asynchronous workflows for parallel or concurrent programming, however these are just more sophisticated uses of F# workflows or libraries built on top of them - they take the advantage of non-blocking behavior described earlier.
You can use StartChild
to start a workflow in background - the method gives you a running workflow that you can use (using let!
) later in the workflow to wait for completion, while you can continue doing other things. This is similar to Tasks in .NET 4.0, but it runs asynchronously, so it is more suitable for I/O operations.
You can use Async.Parallel
to create multiple workflows and wait until all of them complete (which is great for data-parallel operations). This is similar to PLINQ, but again, async
is better if you do some I/O operations.
Finally, you can use MailboxProcessor
which allows you to write concurrent applications using the message-passing style (Erlang style). This is a great alternative to threads for many problems.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With