响应式编程是指确保程序对事件或输入做出响应的做法。在这一节,我们将专注于图形界面方面的响应式编程,图形界面总量响应式的。然而,其他网格的编程也需要考虑响应式编程,例如,运行在服务器上的程序总是需要保持对输入作出响应,即使是在它处理其他需要长时间运行的任务期间。我们将在第十一章实现聊天服务器时,会看到在服务器编程方面也要用到这里讨论的一些方法。
大多数图形界面库使用事件循环去处理绘制图形界面,以及与用户之间的交互,就是说,一个线程既要负责绘制图形界面,也要处理其上的所有事件,我们称这种线程为图形界面线程。另一种考虑:只应该用图形界面线程更新图形界面对象,要避免发生其他可能破坏这个图形界面线程状态的情况出现,即,在其他线程中非常耗时的计算或输入输出操作,不应该出现在图形界面线程中。如果这个图形界面线程需要进行长时间运行的计算,它既不可能与用户交互,也不可绘制图形界面,这就是图形界面反应迟钝的头号原因。
可以看到,在下面的示例中创建的图形界面很容易就能变得没有反映,因为在这个图形界面线程中有太多的计算。我们首选来看一下一个有用的抽象概念,后台辅助线程(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 Strangelights.Extensions
open System
open System.ComponentModel
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))
//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())
//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)
使用后台辅助线程只要对代码做很少的修改,把代码分成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 Strangelights.Extensions
open System
open System.ComponentModel
open System.Windows.Forms
open System.Numerics
// define a type to hold the results
type Result =
{Input: int;
Fibonacci:BigInteger; }
let form =
letform = new Form()
//input text box
letinput = new TextBox()
//button to launch processing
letbutton = new Button(Left = input.Right + 10, Text = "Go")
//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()
//hook up creating and running the worker to the button
button.Click.Add(fun_ -> runWorker())
//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-2 所示:
图 10-2 更好地适应多线程编程的图形界面
响应式编程(Reactive programming)