开发者

Understanding F# Asynchronous Programming

开发者 https://www.devze.com 2022-12-22 18:20 出处:网络
I kind of know the syntax of asynchronous programming in F#. E.g. let downloadUrl(url:string) = async {

I kind of know the syntax of asynchronous programming in F#. E.g.

let downloadUrl(url:string) = async { 
  let req = HttpWebReques开发者_开发知识库t.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.


It's not really about "time gained." Asynchronous programming won't make the data arrive any faster. Rather, it's about simplifying the mental model for concurrency.

In C#, for example, if you want to perform an async operation, you need to start mucking around with callbacks, and passing local state to those callbacks, and so on. For a simple operation like the one in Expert F# with two async operations, you're looking at three seemingly separate methods (the initiator and two callbacks). This disguises the sequential, conceptually linear nature of the workflow: do request, read stream, print results.

By contrast, the F# async workflow code makes the sequencing of the program very clear. You can tell exactly what is happening in what order just by looking at the one block of code. You don't need to chase callbacks.

That said, F# does have mechanisms which can help save time if there are several independent async operations in progress. For example, you can kick off multiple async workflows at the same time, and they will run in parallel. But within a single async workflow instance, it's primarily about simplicity, safety and understandability: about letting you reason about asynchronous sequences of statements as easily as you reason about C#-style synchronous sequences of statements.


This is a good question. It is important to note that multiple statements in an async blocks are not run in parallel. async blocks essentially yield processor time to other processes while asynchronous requests are pending. So an async block won't generally run any faster than an equivalent sequence of synchronous operations, but it will allow more work to occur overall. If you're looking to run multiple statements in parallel, you'd be better off looking at the Task Parallel Library.

0

精彩评论

暂无评论...
验证码 换一张
取 消