天天看點

異步程式設計(Asynchronous Programming)

異步程式設計(Asynchronous Programming)

異步程式設計與我們所看過的其他并行程式設計形式有一些不同,讨論的其他主題可以有大量的線程并行運作,可以完全利用系統中可用的處理器;而在異步程式設計中,需要避免阻塞線程,我們在這一章的第一節“線程、記憶體、鎖定和阻塞”中已經對阻塞線程的概念有所了解了。阻塞的線程是不能工作的線程,因為它需要等待其他任務的完成;線程等待的通常任務是作業系統執行的輸入輸出,但有時也可能是等待鎖,是以會進行臨界區。線程是相對昂貴的資源,每個線程配置設定 1 MB 的堆(stack),以及作業系統核心為管理大量線程而産生的其他相關消耗。在性能至上的代碼中,保持阻塞線程數量在較低的水準上是至關重要的;理論上,隻要做到有多少的處理器,就有多少的線程,就不會有阻塞的線程了。

注意

為了概要地了解一下用這些方法能夠達到什麼樣的結果,可以看一下Amanda Laucher 2009 年在 Lang.NET 上的演講,她講解了如何使用F# 工作流并行化 C# 的程式,以及實作了一些令人印象深刻的結果:。

在這一節,我們将學習如何使用 .NET 架構的異步程式設計模型(asynchronous programming model)避免輸入輸出期間線程的阻塞。異步程式設計模型的意思是,在有關流的類上,使用一對Begin/End 方法,比如 BeginRead/EndRead;典型地,這一對方法執行某種輸入輸出任務,比如讀檔案。這種程式設計方法的名聲很不好,因為需要找到能在Begin/End 調用之間保持狀态的好方法。這一節我們直接讨論程式設計模型,相反,将看一下 F# 的一個功能,異步工作流(asynchronous workflows),看如何用它來避免在其他

.NET 語言中與使用異步程式設計模型相關的工作。為了更詳細了解異步程式設計模型,以及使用的困難,請參考Jeffrey Richter 在 MSDN 上的文章《Asynchronous Device Operations》(http://msdn.microsoft.com/en-us/magazine/cc163415.aspx)。

異步工作流不是由 .NET 的異步程式設計模型所專用。在下一節“消息傳遞”中,我們将學習如何使用這些工作流與 F# 的郵箱(mailboxes)來協調大量不同的任務,它可以等待任務完成而不阻塞線程。

了解 F# 中異步工作流的第一步是了解它的文法。建立異步工作流,使用一進制文法(monadic syntax),同我們在第三章中見過的序清單達式相似;基本文法使用關鍵字async,加用大括号括起來的工作流表達式:async { ... }。簡單的工作流程式像這樣使用工作流:

open System.IO

// a function to read a text fileasynchronusly

let readFile file =

  async{ let! stream = File.AsyncOpenText(file)

         let! fileContents = stream.AsyncReadToEnd()

         return fileContents }

// create an instance of the workflow

let readFileWorkflow = readFile"mytextfile.txt"

// invoke the workflow and get the contents

let fileContents = Async.RunSynchronouslyreadFileWorkflow

編譯這個程式,需要引用 FSharp.PowerPack.dll。程式中的 readFile 函數建立一個工作,異步讀檔案,然後傳回檔案的内容;接下來,建立工作流的執行個體 readFileWorkflow;最後,運作這個工作流,獲得檔案的内容。很重要的一點,是要了解,隻調用 readFile 函數并不真正讀檔案;相反,它報建工作流的新執行個體,然後,運作這個工作流,去執行讀檔案的任務;Async.RunSynchronously 函數真正負責運作工作流。工作流執行個體是一種小型的資料結構,有點像一段小程式,能夠解釋一些要做的工作。

關注這個示例最重要的是 let 後面的感歎号(let!),通常讀作let bang。工作流/一進制文法可以為 let! 賦予不同的含義。在異步工作流中,它表示将要發生的異步操作;在異步操作發生期間工作流停止運作;線上程池中插入一個回調函數,當這個異步操作完成時被調用,如果發出原始調用的線程沒有空閑,就可能發生在不同的線程上;異步調用之後,原始線程被釋放,可以繼續其他工作。

你可能已經注意到,let! 是用在有 Async字首的一些專用方法,在 FSharp.PowerPack.dll 中,這些函數被定義成類型擴增(type augmentations),它與 C# 的擴充方法(extension

methods)等價,這些方法處理對 Begin/End方法對的調用。如果沒有現成的 Async 方法,我們自己建立也很簡單,使用 Async.Primitive 函數和 Begin/End 方法對。

簡單的步驟可能像這樣:

第一步:主程式線程啟動打開檔案流的程序,線上程池中插入回調函數,當這個操作完成時使用,而這個線程現在空閑可以繼續做其他工作;

第二步:當檔案流已經打開,線程池線程(A thread pool thread)被激活,開始讀檔案的内容,線上程池中插入回調函數,當這個操作完成時使用。因為它是一個線程池線程,是以,它将傳回到線程池;

第三步:當已經完成讀檔案,線程池線程被激活,将傳回從檔案中讀到的文本資料,并傳回到線程池;

第四步:因為我們已經使用了 sync.RunSynchronously 函數,主程式線程将等待工作流的結果,接收檔案的内容。

在這個簡單的示例中,你還可能會發現一點缺陷,沒有阻塞主程式線程等待輸入輸出,但是,因為我們等待異步工作流完成,也就阻塞了主程式線程,直到輸入輸出完成。換一種方式,在它自己的[ 線程嗎? ]上運作異步工作流并等待結果,就沒有或幾乎沒有優勢了。然而,并行運作幾個工作流相當簡單;同時運作幾個工作流有一個明顯的優勢,因為原始線程在它啟動了第一個異步任務之後,不會被阻塞,就是說,它是空閑的,可以繼續運作其他異步任務。

要說明這個也很簡單,我們把原來的示例作一點修改,不是讀一個檔案,而是讀三個檔案。而把這個與同步版本的程式作一下比較,有助于發現它們之間的差别。我們先看一下同步版本:

open System

open System.Threading

let print s =

  lettid = Thread.CurrentThread.ManagedThreadId

  Console.WriteLine(sprintf"Thread %i: %s" tid s)

let readFileSync file =

  print(sprintf "Beginning file %s" file)

  letstream = File.OpenText(file)

  letfileContents = stream.ReadToEnd()

  print(sprintf "Ending file %s" file)

  fileContents

let filesContents =

  [|readFileSync "text1.txt";

     readFileSync"text2.txt";

     readFileSync"text3.txt"; |]

這個程式相當簡單,其中還有一些調試代碼,顯示處理檔案的開始與結束。現在再看一下異步版本:

let readFileAsync file =

  async{ do print (sprintf "Beginning file %s" file)

         let! stream = File.AsyncOpenText(file)

         do print (sprintf "Ending file %s" file)

  Async.RunSynchronously

    (Async.Parallel[ readFileAsync "text1.txt";

                     readFileAsync "text2.txt";

                     readFileAsync "text3.txt"; ])

另外,這個版本也包含了一些調試代碼,是以,可以看到程式是如何運作的。最大的改變是現在使用了 Async.Parallel函數,把幾個工作流組合成一個工作流。這樣,當第一個線程完成處理第一個異步調用之後,就空閑了,可以繼續處理其他工作流。看看下面兩個程式的運作結果就知道了:

同步結果:

Thread 1: Beginning file text1.txt

Thread 1: Ending file text1.txt

Thread 1: Beginning file text2.txt

Thread 1: Ending file text2.txt

Thread 1: Beginning file text3.txt

Thread 1: Ending file text3.txt

異步結果:

Thread 3: Beginning file text1.txt

Thread 4: Beginning file text2.txt

Thread 3: Beginning file text3.txt

Thread 4: Ending file text2.txt

Thread 4: Ending file text1.txt

Thread 4: Ending file text3.txt

兩組結果完全不同。對于同步結果,每一個 Beginning file 後面跟一個 Ending file,且出現在同一個線程中;第二種情況下,所有 Beginningfile 的執行個體同時發生,且在兩個不同的線程中,這是因為每一個線程完成了異步操作以後,它就空閑了可以繼教啟動另一個操作。輸入輸出一旦完成之後,Ending file 就發生了。