響應式程式設計(Reactive programming)
響應式程式設計是指確定程式對事件或輸入做出響應的做法。在這一節,我們将專注于圖形界面方面的響應式程式設計,圖形界面總量響應式的。然而,其他網格的程式設計也需要考慮響應式程式設計,例如,運作在伺服器上的程式總是需要保持對輸入作出響應,即使是在它處理其他需要長時間運作的任務期間。我們将在第十一章實作聊天伺服器時,會看到在伺服器程式設計方面也要用到這裡讨論的一些方法。
大多數圖形界面庫使用事件循環去處理繪制圖形界面,以及與使用者之間的互動,就是說,一個線程既要負責繪制圖形界面,也要處理其上的所有事件,我們稱這種線程為圖形界面線程。另一種考慮:隻應該用圖形界面線程更新圖形界面對象,要避免發生其他可能破壞這個圖形界面線程狀态的情況出現,即,在其他線程中非常耗時的計算或輸入輸出操作,不應該出現在圖形界面線程中。如果這個圖形界面線程需要進行長時間運作的計算,它既不可能與使用者互動,也不可繪制圖形界面,這就是圖形界面反應遲鈍的頭号原因。
可以看到,在下面的示例中建立的圖形界面很容易就能變得沒有反映,因為在這個圖形界面線程中有太多的計算。我們首選來看一下一個有用的抽象概念,背景輔助線程(BackgroundWorker)類,在System.ComponentModel 命名空間下,這個類能夠運作一些工作,當工作完成時,觸發通告(notification)事件。這對于圖形界面程式設計非常有用,因為完成的通告由圖形界面線程觸發,有助于強制執行圖形界面對象隻應該由建立它的線程進行更改的規則。
特别地,這個示例建立了計算斐波納契數列,使用的是第們在第七章介紹的斐波納契算法:
module Strangelights.Extensions
let fibs =
(0I,1I)|> Seq.unfold
(fun(n0, n1) ->
Some(n0, (n1, n0 + n1)))
let fib n = Seq.nth n fibs
[
起始值應該是(0, 1)
]
為這個文教二個圖形界面也很簡單,可以使用我們在第八間介紹的 Windows 窗體圖形界面工具:
open Strangelights.Extensions
open System
open System.Windows.Forms
let form =
letform = new Form()
//input text box
letinput = new TextBox()
//button to launch processing
letbutton = new Button(Left = input.Right + 10, Text = "Go")
//label to display the result
letoutput = new Label(Top = input.Bottom + 10, Width = form.Width,
Height = form.Height -input.Bottom + 10,
Anchor = (AnchorStyles.Top||| AnchorStyles.Left |||
AnchorStyles.Right||| AnchorStyles.Bottom))
//do all the work when the button is clicked
button.Click.Add(fun_ ->
output.Text<- Printf.sprintf "%A" (fib (Int32.Parse(input.Text))))
//add the controls
letdc c = c :> Control
form.Controls.AddRange([|dcinput; dc button; dc output |])
//return the form
form
// show the form
do Application.Run(form)
運作後建立的圇界面如圖 10-1:
圖 10-1 計算斐波納契數列的圖形界面
這個圖形界面以合理的方式顯示計算結果,但是很不幸,一旦計算時間變長,圖形界面就變得沒有反映了。下面的代碼就是造成不反映的原因:
// do all the work when the button isclicked
button.Click.Add(fun _ ->
output.Text<- Printf.sprintf "%A" (fib (Int32.Parse(input.Text))))
這段代碼表示我們做的所有計算與觸發單擊事件是在同一個線程中,即圖形界面線程,就是說,圖形界面線程是負責計算的,在執行計算期間,就不可能處理其他事件。
把它改成使用背景輔助線程是相當容易:
open System.ComponentModel
//create and run a new background worker
letrunWorker() =
letbackground = new BackgroundWorker()
//parse the input to an int
letinput = Int32.Parse(input.Text)
//add the "work" event handler
background.DoWork.Add(funea ->
ea.Result<- fib input)
//add the work completed event handler
background.RunWorkerCompleted.Add(funea ->
output.Text <- Printf.sprintf"%A" ea.Result)
//start the worker off
background.RunWorkerAsync()
//hook up creating and running the worker to the button
button.Click.Add(fun_ -> runWorker())
使用背景輔助線程隻要對代碼做很少的修改,把代碼分成DoWork 和 RunWorkerCompleted 事件,再稍許寫一點代碼,但除此之外,再不要求其他的代碼了。我們就看看需要修改的代碼,首選建立背景輔助線程類的執行個體:
let background = new BackgroundWorker()
把這個代碼放在需要在背景運作的其他線程中,即在DoWork 事件中;還需要小心從DoWork 的控件外提取所有需要的資料;因為這個代碼發生在不同的線程中,使代碼與圖形界面對象進行互動可能打破隻由圖形界面線程管理的規則。下面的代碼用于讀整數,并傳給DoWork 事件:
// parse the input to an int
let input = Int32.Parse(input.Text)
// add the "work" event handler
background.DoWork.Add(fun ea ->
ea.Result<- fib input)
在前面的示例中,從文本框中提取整數,并剛好在把事件處理程式添加到DoWork 事件之前進行解析;接下來,添加到DoWork 事件中的lambda 函數捕捉到這個整數結果,應該把這個結果放在DoWork 事件中的Result 屬性中,成為事件參數;然後,在RunWorkerCompleted 事件中恢複這個屬性中的值。它們兩個都有 Result屬性,如下面代碼所示:
// add the work completed event handler
background.RunWorkerCompleted.Add(fun ea->
output.Text<- Printf.sprintf "%A" ea.Result)
RunWorkerCompleted 事件當然可以運作在圖形界面線程中,是以,很容易和圖形界面對象進行互動。我們已經把事件都連接配接好了,但還餘下兩個任務:第一,需要啟動背景輔助線程:
// start the worker off
background.RunWorkerAsync()
第二,需要把所有這些代碼添加到按鈕的單擊事件中。我們已經把前面的代碼包裝到一個函數runWorker() 中,是以,在事件處理程式中調用這個代碼就很簡單了:
// hook up creating and running the workerto the button
button.Click.Add(fun _ -> runWorker())
注意,這表示每次單擊按鈕就建立一個新的背景輔助線程,這是因為背景輔助線程一旦使用,就不能重用。
現在,不管你單擊多少次 Go 按鈕,圖形界面都能響應。但這也導緻了其他問題,例如,很容易就能啟動兩次計算,這都是需要花費一些時間才能完成的。如果發生了這種情況,兩次結果都會放在同一個結果标簽中,這樣,使用者不可能知道哪一個是先完成的,當看到時,已經顯示出來了。圖形界面能保持響應,但并不能很好地适應多線程網格的程式設計,一種解決方案是在計算期間禁用所有控件,對某些情況,這可能是合适的,但是,整體來講,這不是很好的解決方案,因為如果這樣使用者就不能很好地利用響應式圖形界面的了。更好的解決方案是建立一個可顯示多個結果的系統,對應其初始參數;這樣,就能保證使用者可以知道結果是什麼含義。這個示例使用資料網格視圖來顯示結果:
open System.Numerics
// define a type to hold the results
type Result =
{Input: int;
Fibonacci:BigInteger; }
//list to hold the results
letresults = new BindingList<Result>()
//data grid view to display multiple results
letoutput = new DataGridView(Top = input.Bottom + 10, Width = form.Width,
Height =form.Height - input.Bottom + 10,
Anchor =(AnchorStyles.Top ||| AnchorStyles.Left |||
AnchorStyles.Right||| AnchorStyles.Bottom),
DataSource =results)
// create and run a new background worker
let runWorker() =
letbackground = new BackgroundWorker()
//parse the input to an int
letinput = Int32.Parse(input.Text)
//add the "work" event handler
background.DoWork.Add(funea ->
ea.Result<- (input, fib input))
//add the work completed event handler
background.RunWorkerCompleted.Add(funea ->
letinput, result = ea.Result :?> (int * BigInteger)
results.Add({Input = input; Fibonacci = result; }))
//start the worker off
background.RunWorkerAsync()
新的圖形界面如圖 10-2 所示:
圖 10-2 更好地适應多線程程式設計的圖形界面