簡體   English   中英

理解F#異步編程

[英]Understanding F# Asynchronous Programming

我有點了解F#中異步編程的語法。 例如

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() }

在F#專家書(和許多其他來源),他們說喜歡

讓! var = expr只是表示“執行異步操作expr並在操作完成時將結果綁定到var。然后繼續執行其余的計算主體”

我也知道在執行異步操作時會創建一個新線程。 我最初的理解是異步操作后有兩個並行線程,一個執行I / O,另一個繼續同時執行異步主體。

但在這個例子中,我很困惑

  let! resp = req.AsyncGetResponse()
  let stream = resp.GetResponseStream()

如果resp尚未啟動並且異步體中的線程想要GetResponseStream什么? 這可能是錯誤嗎?

也許我原來的理解是錯誤的。 F#專家書中引用的句子實際上意味着“創建一個新線程,掛起當前線程,當新線程完成時,喚醒正文線程並繼續”,但在這種情況下我看不到我們可以保存任何時候。

在最初的理解中,當一個異步塊中有多個獨立的 IO操作時,可以節省時間,這樣它們可以在不相互干預的情況下同時完成。 但是在這里,如果我沒有得到響應,我就無法創建流; 只有我有流,我可以開始閱讀流。 獲得的時間在哪里?

此示例中的“異步”不是關於並發或節省時間,而是關於提供良好的編程模型而不阻塞(讀取:浪費)線程。

如果使用其他編程語言,通常有兩種選擇:

您可以阻止 ,通常通過調用同步方法。 缺點是線程在等待磁盤或網絡I / O或您擁有的東西時被占用並且沒有做任何有用的工作。 優點是代碼簡單(普通代碼)。

您可以使用回調來異步調用,並在操作完成時收到通知。 優點是您不會阻塞線程(這些線程可以返回到例如ThreadPool,並且當操作完成后將使用新的ThreadPool線程來回調)。 缺點是一個簡單的代碼塊被分成一堆回調方法或lambda,並且很快就會在回調中維護狀態/控制流/異常處理變得非常復雜。

所以你在岩石和堅硬的地方之間; 你要么放棄簡單的編程模型,要么浪費線程。

F#模型提供了兩全其美的效果; 你不會阻止線程,但你保持簡單的編程模型。 let! 使您能夠在異步塊的中間“跳線”,所以在代碼中

Blah1()
let! x = AsyncOp()
Blah2()

Blah1可以在ThreadPool線程#13上運行,但隨后AsyncOp會將該線程釋放回ThreadPool。 稍后當AsyncOp完成時,其余代碼將在可用線程(可能是ThreadPool線程#20)上開始備份,該線程將x綁定到結果,然后運行Blah2 在簡單的客戶端應用程序中,這很少重要(除非確保您不阻止UI線程),但在執行I / O的服務器應用程序中(線程通常是寶貴的資源 - 線程很昂貴,您不能浪費它們阻塞)非阻塞I / O通常是使應用程序擴展的唯一方法。 F#使您能夠編寫非阻塞I / O,而無需將程序降級為大量的意大利面條代碼回調。

也可以看看

使用異步工作流並行化的最佳實踐

如何在F#中進行鏈式回調?

http://cs.hubfs.net/forums/thread/8262.aspx

我認為理解異步工作流最重要的是它們的順序與用F#編寫的普通代碼(或C#,就此而言)順序相同。 你有一些let綁定,以通常的順序和一些表達式(可能有副作用)進行評估。 實際上, 異步工作流通常看起來更像命令式代碼。

異步工作流的第二個重要方面是它們是非阻塞的 這意味着您可以執行以某種非標准方式執行的操作,並且在執行時不會阻塞該線程。 (一般來說, let!在F#計算表達式中始終表示存在一些非標准行為 - 如果沒有在Maybe monad中產生結果,則可能會失敗,或者它可能是異步工作流的非阻塞執行)。

從技術上講,非阻塞執行是通過注冊一些將在操作完成時觸發的回調來實現的。 相對簡單的示例是等待某個指定時間的異步工作流 - 這可以使用Timer實現而不會阻塞任何線程(示例來自我的書的第13章, 源代碼可在此處獲得 ):

// 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() )

還有幾種方法可以將F#異步工作流用於並行或並發編程,但這些只是F#工作流或基於它們構建的庫的更復雜的用法 - 它們利用了前面描述的非阻塞行為。

  • 您可以使用StartChild在后台啟動工作流 - 該方法為您提供了一個正在運行的工作流,您可以在工作流中稍后使用(使用let! )等待完成,同時您可以繼續執行其他操作。 這類似於.NET 4.0中的任務 ,但它以異步方式運行,因此更適合I / O操作。

  • 您可以使用Async.Parallel創建多個工作流並等待所有工作流完成(這對於數據並行操作非常Async.Parallel )。 這類似於PLINQ,但同樣,如果進行一些I / O操作, async會更好。

  • 最后,您可以使用MailboxProcessor ,它允許您使用消息傳遞樣式(Erlang樣式)編寫並發應用程序。 對於許多問題,這是線程的一個很好的替代方案。

這不是關於“獲得的時間”。 異步編程不會使數據更快地到達。 相反,它是關於簡化並發的心智模型。

例如,在C#中,如果要執行異步操作,則需要開始使用回調,並將本地狀態傳遞給這些回調,依此類推。 對於像Expert F#中的兩個異步操作這樣的簡單操作,您將看到三個看似獨立的方法(啟動器和兩個回調)。 這掩蓋了工作流的順序概念線性特性:請求,讀取流,打印結果。

相比之下,F#異步工作流程代碼使程序的排序非常清晰。 只需查看一個代碼塊,就可以確切地知道發生了什么。 你不需要追逐回調。

也就是說,如果正在進行多個獨立的異步操作,F#確實有一些機制可以幫助節省時間。 例如,您可以同時啟動多個異步工作流,它們將並行運行。 但是在單個異步工作流實例中,它主要是關於簡單性,安全性和可理解性:關於讓你理解異步語句序列就像你推理C#式同步語句序列一樣容易。

這是一個很好的問題。 請務必注意, async塊中的多個語句不是並行運行的。 當異步請求處於掛起狀態時, async塊實質上會為其他進程提供處理器時間 因此, async塊通常不會比等效的同步操作序列運行得更快,但它將允許更多的工作總體發生。 如果您希望並行運行多個語句,那么最好不要查看任務並行庫。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM