2010-08-22 51 views
31

我想了解如何在F#中使用asynclet!。 我讀過的所有文檔都顯得令人困惑。 使用Async.RunSynchronously運行異步塊有什麼意義?這是異步還是同步?看起來像是一個矛盾。F#的異步真的如何工作?

該文檔說Async.StartImmediate在當前線程中運行。如果它運行在同一個線程中,它看起來對我來說不是非常異步的......或者異步更像是協程而不是線程。如果是這樣,他們什麼時候退讓?

引用MS文檔:

的代碼,使用我們行!開始計算,然後線程暫停 ,直到結果可用,此時執行繼續。

如果線程等待結果,爲什麼我應該使用它?看起來像普通的舊函數調用。

Async.Parallel做什麼?它接收到一系列Async <'T>。爲什麼不能並行執行一系列普通函數?

我想我錯過了這裏非常基本的東西。我想,在瞭解之後,所有文檔和示例都將開始有意義。

+0

http://stackoverflow.com/questions/2444676/understanding-f-asynchronous-programming – 2010-09-07 17:17:58

回答

30

有幾件事。

首先,

let resp = req.GetResponse() 

let! resp = req.AsyncGetReponse() 

之間的區別是,對於可能數百毫秒(一個永恆的CPU),其中web請求是「海上」時,前者使用一個線程(在I/O上被阻塞),而後者則使用線程。這是異步最常見的'勝利':您可以編寫非阻塞I/O,這樣不會浪費任何等待硬盤旋轉的線程或網絡請求返回。 (不像大多數其它語言,你不會被強迫做的控制和係數東西反轉成回調。)

其次,Async.StartImmediate開始在當前線程上異步。一個典型的用途是使用GUI,你有一些GUI應用程序想要例如更新UI(例如在某處說「加載...」),然後執行一些後臺工作(從磁盤或其他任何地方加載內容),然後返回到前臺UI線程以在完成時更新UI(「完成!」) )。StartImmediate允許異步在操作開始時更新UI,並捕獲SynchronizationContext,以便在操作結束時可以返回GUI以執行UI的最終更新。

接下來,Async.RunSynchronously很少使用(有一篇論文是您在任何應用程序中最多稱過一次)。在極限情況下,如果您將整個程序寫入異步,那麼在「main」方法中,您可以調用RunSynchronously來運行程序並等待結果(例如,將結果打印在控制檯應用程序中)。這確實會阻塞一個線程,所以它通常只在程序的異步部分的「頂部」有用,並且在同步內容的邊界上是有用的。 (更先進的用戶可能更StartWithContinuations - RunSynchronously是有點「簡單的黑客」從異步回同步來獲得。)

最後,Async.Parallel確實的fork-join並行性。你可以編寫一個類似的函數,而不是使用函數而不是async(像TPL中的東西),但F#中典型的甜蜜點是並行I/O限制計算,它們已經是異步對象,所以這是最常見的有用的簽名。 (對於CPU綁定並行性,您可以使用異步,但也可以使用TPL。)

+1

謝謝!爲您在帖子中提到的關鍵詞搜索,我在這裏找到了 http://blogs.msdn.com/b/dsyme/archive/2009/10/19/release-notes-for-the-f-october-2009- release.aspx表示某些操作具有「自動返回上下文」,因此GUI操作更容易實現。但是,如果程序員不瞭解這個特性,它會變得非常混亂,因爲乍一看,代碼似乎被破壞了,但它仍然有效。 – marcus 2010-08-23 00:44:43

+1

是的,在普通情況下的便利性和代碼/線程模型的整體透明度/透明度之間肯定存在折衷。 – Brian 2010-08-23 01:44:31

+1

只需對「StartImmediate」啓用捕獲「SynchronizationConnect」的聲明發表評論即可。它不會隱式地啓用捕獲,但只是允許用戶在計算表達式的部分同步部分中設置同步上下文,然後再執行諸如「Async.SwitchToContext(syncContext)'之類的操作來切換回UI線程。 – kasperhj 2015-06-17 18:07:12

12

異步的用法是保存使用中的線程數。

請看下面的例子:

let fetchUrlSync url = 
    let req = WebRequest.Create(Uri url) 
    use resp = req.GetResponse() 
    use stream = resp.GetResponseStream() 
    use reader = new StreamReader(stream) 
    let contents = reader.ReadToEnd() 
    contents 

let sites = ["http://www.bing.com"; 
      "http://www.google.com"; 
      "http://www.yahoo.com"; 
      "http://www.search.com"] 

// execute the fetchUrlSync function in parallel 
let pagesSync = sites |> PSeq.map fetchUrlSync |> PSeq.toList 

上面的代碼是你想要做什麼:定義一個函數和並行執行。那麼爲什麼我們需要異步呢?

讓我們考慮一件大事。例如。如果網站的數量不是4,而是說,10,000!那麼需要10,000個線程並行地運行它們,這是巨大的資源成本。

雖然在異步:

let fetchUrlAsync url = 
    async { let req = WebRequest.Create(Uri url) 
      use! resp = req.AsyncGetResponse() 
      use stream = resp.GetResponseStream() 
      use reader = new StreamReader(stream) 
      let contents = reader.ReadToEnd() 
      return contents } 
let pagesAsync = sites |> Seq.map fetchUrlAsync |> Async.Parallel |> Async.RunSynchronously 

當代碼是use! resp = req.AsyncGetResponse(),當前線程放棄其資源可用於其他用途。如果響應在1秒內回來,那麼你的線程可以使用這1秒來處理其他的東西。否則線程被阻塞,浪費線程資源1秒鐘。

因此,即使您是以異步方式並行下載10000個網頁,線程數量也僅限於少量。

我想你不是一個.Net/C#程序員。異步教程通常假設人們知道.Net以及如何在C#中編寫異步IO(很多代碼)。 F#中異步構造的魔力並不是爲了並行。因爲簡單的平行可以通過其他構造來實現,例如, Parallel.Net中的並行擴展。但是,異步IO更復雜,因爲您看到線程放棄其執行,當IO完成時,IO需要喚醒其父線程。這是異步魔法的用處:在幾行簡明的代碼中,您可以執行非常複雜的控制。

+0

我不認爲你回答了正確的問題。 – Gabe 2010-08-22 03:51:06

+0

我想我是對的。我從他的問題開始:「爲什麼不是一系列普通函數要並行執行?」。所以我預測他是.Net的新手,因此需要看到異步IO的動機。 – 2010-08-22 04:05:31

+1

.NET和Parallel Framework擴展庫之間有一個區別,這個問題更多的是後者。 – 2010-08-22 04:10:34

1

let!Async.RunSynchronously背後的想法是,有時你有一個異步活動,你需要在繼續之前得到結果。例如,「下載網頁」功能可能沒有同步的功能,因此您需要某種方式來同步運行它。或者如果你有一個Async.Parallel,你可能會有數百個任務同時發生,但是你希望他們在完成之前完成所有任務。

據我所知,您使用Async.StartImmediate的原因是您有一些計算需要在當前線程(可能是UI線程)上運行而不會阻塞它。它使用協程?我想你可以這麼稱呼,雖然在.Net中沒有一個通用的協同機制。

那麼爲什麼Async.Parallel需要一個序列Async<'T>?可能是因爲它是組成Async<'T>對象的一種方式。你可以很容易地創建自己的抽象,它只與普通函數(或簡單函數和Async s的組合)一起工作,但它只是一個方便的功能。

0

在異步模塊中,可以有一些同步和一些異步操作,所以,舉例來說,你可能有一個網站會以幾種方式顯示用戶的狀態,所以你可以顯示他們是否有很快到期的賬單,生日和作業到期。這些都不在同樣的數據庫,所以你的應用程序會進行三次獨立的調用,你可能想要並行地調用這些調用,這樣當最慢的調用完成時,你可以把結果放在一起並顯示出來,所以最終的結果是顯示是基於最慢的,你不關心這些返回的順序,你只是想知道什麼時候收到了這三個。

爲了完成我的示例,您可能想要同步執行創建UI以顯示此信息的工作。因此,最後,您希望獲取這些數據並顯示UI,順序無關緊要的部分是並行完成的,並且順序問題可以以同步方式完成。

您可以將它們作爲三個線程來完成,但是當第三個線程完成後,您必須保持跟蹤並取消暫停原始線程,但這樣做更有效,.NET框架更容易處理此問題。

5

最近我對異步模塊中的函數進行了簡要概述:here。也許它會有所幫助。

+0

非常有幫助,謝謝!但是我需要一些時間才能完全理解這一切! :-) – marcus 2010-08-23 00:17:02

8

這裏有很多很好的答案,但我認爲我對這個問題採取了不同的角度:F#的異步真的如何工作?

與C#F#中的async/await不同,開發人員實際上可以實現自己的版本Async。這可以是瞭解Async如何工作的好方法。

(對於感興趣的源代碼Async可以在這裏找到:https://github.com/Microsoft/visualfsharp/blob/fsharp4/src/fsharp/FSharp.Core/control.fs

由於我們的基本構建模塊爲我們的DIY工作流程,我們定義:

type DIY<'T> = ('T->unit)->unit 

這是接受另一個函數函數(稱爲繼續),在'T類型的結果準備就緒時調用。這允許DIY<'T>在不阻塞調用線程的情況下啓動後臺任務。當結果準備好時,繼續被調用,允許計算繼續。

該F#Async構建塊有點複雜,因爲它還包括取消和異常延續,但本質上就是這樣。

爲了支持F#工作流語法,我們需要定義一個計算表達式(https://msdn.microsoft.com/en-us/library/dd233182.aspx)。雖然這是一個相當先進的F#功能,但它也是F#最令人驚喜的功能之一。定義的兩個最重要的操作是return & bind,F#使用它們將我們的DIY<_>構造塊組合到聚合的DIY<_>構件中。

adaptTask用於將Task<'T>修改爲DIY<'T>startChild允許啓動幾個simulatenous DIY<'T>,請注意,它不啓動新線程爲了這樣做,但重新使用調用線程。

沒有這裏的任何進一步的ADO的示例程序:

open System 
open System.Diagnostics 
open System.Threading 
open System.Threading.Tasks 

// Our Do It Yourself Async workflow is a function accepting a continuation ('T->unit). 
// The continuation is called when the result of the workflow is ready. 
// This may happen immediately or after awhile, the important thing is that 
// we don't block the calling thread which may then continue executing useful code. 
type DIY<'T> = ('T->unit)->unit 

// In order to support let!, do! and so on we implement a computation expression. 
// The two most important operations are returnValue/bind but delay is also generally 
// good to implement. 
module DIY = 

    // returnValue is called when devs uses return x in a workflow. 
    // returnValue passed v immediately to the continuation. 
    let returnValue (v : 'T) : DIY<'T> = 
     fun a -> 
      a v 

    // bind is called when devs uses let!/do! x in a workflow 
    // bind binds two DIY workflows together 
    let bind (t : DIY<'T>) (fu : 'T->DIY<'U>) : DIY<'U> = 
     fun a -> 
      let aa tv = 
       let u = fu tv 
       u a 
      t aa 

    let delay (ft : unit->DIY<'T>) : DIY<'T> = 
     fun a -> 
      let t = ft() 
      t a 

    // starts a DIY workflow as a subflow 
    // The way it works is that the workflow is executed 
    // which may be a delayed operation. But startChild 
    // should always complete immediately so in order to 
    // have something to return it returns a DIY workflow 
    // postProcess checks if the child has computed a value 
    // ie rv has some value and if we have computation ready 
    // to receive the value (rca has some value). 
    // If this is true invoke ca with v 
    let startChild (t : DIY<'T>) : DIY<DIY<'T>> = 
     fun a -> 
      let l = obj() 
      let rv = ref None 
      let rca = ref None 

      let postProcess() = 
       match !rv, !rca with 
       | Some v, Some ca -> 
        ca v 
        rv := None 
        rca := None 
       | _ , _ ->() 

      let receiver v = 
       lock l <| fun() -> 
        rv := Some v 
        postProcess() 

      t receiver 

      let child : DIY<'T> = 
       fun ca -> 
        lock l <| fun() -> 
         rca := Some ca 
         postProcess() 

      a child 

    let runWithContinuation (t : DIY<'T>) (f : 'T -> unit) : unit = 
     t f 

    // Adapts a task as a DIY workflow 
    let adaptTask (t : Task<'T>) : DIY<'T> = 
     fun a -> 
      let action = Action<Task<'T>> (fun t -> a t.Result) 
      ignore <| t.ContinueWith action 

    // Because C# generics doesn't allow Task<void> we need to have 
    // a special overload of for the unit Task. 
    let adaptUnitTask (t : Task) : DIY<unit> = 
     fun a -> 
      let action = Action<Task> (fun t -> a()) 
      ignore <| t.ContinueWith action 

    type DIYBuilder() = 
     member x.Return(v) = returnValue v 
     member x.Bind(t,fu) = bind t fu 
     member x.Delay(ft) = delay ft 

let diy = DIY.DIYBuilder() 

open DIY 

[<EntryPoint>] 
let main argv = 

    let delay (ms : int) = adaptUnitTask <| Task.Delay ms 

    let delayedValue ms v = 
     diy { 
      do! delay ms 
      return v 
     } 

    let complete = 
     diy { 
      let sw = Stopwatch() 
      sw.Start() 

      // Since we are executing these tasks concurrently 
      // the time this takes should be roughly 700ms 
      let! cd1 = startChild <| delayedValue 100 1 
      let! cd2 = startChild <| delayedValue 300 2 
      let! cd3 = startChild <| delayedValue 700 3 

      let! d1 = cd1 
      let! d2 = cd2 
      let! d3 = cd3 

      sw.Stop() 

      return sw.ElapsedMilliseconds,d1,d2,d3 
     } 

    printfn "Starting workflow" 

    runWithContinuation complete (printfn "Result is: %A") 

    printfn "Waiting for key" 

    ignore <| Console.ReadKey() 

    0 

程序的輸出應該是這樣的:

Starting workflow 
Waiting for key 
Result is: (706L, 1, 2, 3) 

當運行程序說明Waiting for key被immidiately打印爲控制檯線程不會阻止啓動工作流程。大約700ms後打印結果。

我希望這有趣的是,一些F#開發者

+1

很有意思,謝謝! – 2016-10-04 11:40:52

2

在許多其他的答案很詳細,但作爲我初學我被C#和F#之間的差異絆倒。

F#異步塊是配方代碼應該如何運行,而不是實際運行它的指令。

你建立你的食譜,也許結合其他食譜(例如Async.Parallel)。只有這樣,您纔要求系統運行它,並且您可以在當前線程(例如Async.StartImmediate)或新任務或其他各種方式上執行此操作。

所以這是你想要做什麼與誰應該做的事情的分離。

C#模型通常被稱爲「熱任務」,因爲任務是作爲其定義的一部分爲您啓動的,而F#的「冷任務」模型則是這些模型。