16.3.1 以异步方式等待事件

来源:互联网 发布:淘宝卖家网页版 编辑:程序博客网 时间:2024/04/27 20:49
16.3.1 以异步方式等待事件


 


    为什么不能使用标准的控制流构造驱动有响应的应用程序,其是原因,我们没有任何方式等待事件的发生。写一个循环运行的函数,并检查事件是否已发生,不仅难于实现,而且也不是好方法:它会阻止执行线程。正如在第 13 章中学习的,使用异步工作流写出来的代码,看起来是顺序式的,但是,它可以包括等待外部事件(比如,异步 I/O 操作的完成),但却以异步方式执行,而不会阻止该线程。


    到目前为止,我们只看到实现  I/O 操作的异步方法,而且也定义了一个基元,它停止异步工作流,当指定的事件发生时,会恢复。这个基元,叫 AwaitObservable,在本书的在线源代码中可 找到,是作为对 Async 类型[2]]的扩展。让我们首先看着它的类型签名:


 


val AwaitObservable : IObservable<'T> -> Async<'T>


 


    类型显示,这个函数是相当简单的,它取一个事件参数,返回一个值,可以让用在异步工作流内,使用 !let 关键字。事件和 Async<'T> 值之间的一个重要区别是,异步工作流最多能够执行一次,虽然可以多次触发事件。这意味着, AwaitObservable 函数只能等待第一次出现的事件,然后,继续进行异步工作流。让我们来看一下,如何在图形用户界面应用程序中使用 AwaitObservable。


 


计数点击鼠标


 


    我们首先实现一个示例,类似于计数器递增/递减的应用程序,用来演示来自 Observable 模块的高阶函数。这将是更简单:它计算点击数,并把计数显示在标签上。可以用 Observable.scan 来实现这个行为,源代码会更短,后面我们会看到,AwaitObservable 是更加强大的构造。清单 16.9 显示了如何使用异步工作流,来写事件处理代码。


 


Listing 16.9 Counting clicks using asynchronous workflows (F#)


 


let frm, lbl = new Form(...), new Label(...)


let rec loop(count) = async {
  let! args = Async.AwaitObservable(lbl.MouseDown)
  lbl.Text <- sprintf "Clicks: %d" count
  return! loop(count + 1) }


do
  Async.StartImmediately(loop(1))
  Application.Run(frm)


 


    实现计数器的应用程序的核心部分是一个递归函数,实现为异步工作流。这个函数好像创建了一个无限循环,这开始听起来很可疑。这个构造是完全有效的,因为它首先等待 MouseDown 事件,它是异步完成的,这意味着,该工作流将安装事件处理程序,而余下的在用户单击标签时,才会执行。一旦事件发生,我们用递增后的计数器更新文本和循环。


    早先我们提到过,AwaitObservable 基元等待第一个事件的发生,因为异步工作流只能产生一个值。如你在此示例中可以看到的,如果我们想要处理事件的每次发生,可以简单地使用递归循环,等待下一次发生。使用递归还可以在函数参数中保存当前状态。事实上,表达计算的这种技术类似于递归函数基元,我们早前在书中讨论过。


    当使用 Windows 窗体控件时,我们需要只从图形用户界面线程中访问它们,这是应用程序的主线程。当尝试从其他线程中使用属性时,其行为是未定义的,应用程序可能崩溃。这意味着,我们需要确保异步工作流只在图形用户界面线程中执行。到目前为止,我们实际上并不在乎工作流在哪儿执行,F# 库提供了一种机制来控制。


    第一,大多数异步操作完成后,返回到调用线程。这意味着,当调用操作,比如,AsyncGetResponse,AsyncRead,或 Async.Sleep,操作将释放调用线程,并开始在后台执行。当它完成时(通常在某个后台线程),将使用  .NET的 SynchronizationContext 类,返回到它开始的线程。


    当我们在图形用户界面线程上启动工作流时,它会继续在图形用户界面线程上运行,即使这个工作流包含一些涉及后台线程的操作。有了这种行为,我们可以从工作流的任何部分,安全地访问 Windows 窗体控件。剩下的唯一问题是,我们如何在图形用户界面线程上,能够启动工作流。在清单 16.9 中,我们使用 Async.StartImmediate 基元,它在当前线程上运行工作流。当应用程序启动时,当前的线程将是图形用户界面的主线程。


    图 16.4 显示,当我们使用 StartImmediate 基元,运行包含调用 AsyncGetResponse 的工作流时,会发生什么。最重要的事实是,当我们运行异步操作(使用 let! 基元)时,图形用户界面线程自由地执行其他工作。当运行在图形用户界面线程上的工作流花的大部分时间,是在等待完成异步操作,这个应用程序不会变得没有响应。






图 16.4 StartImmediate 在图形用户界面线程上启动工作流,AsyncGetResponse 操作在后台运行,而图形用户界面线程可以执行其他工作。当后台操作完成后,这个工作流返回到图形用户界面线程。


 


    正如我们刚才所说的,通过使用 Observable.scan,可以很容易实现这个示例。让我们看一个稍微复杂的问题。


 


限制点击的速度


 


    假设说我们希望限制点击率,希望计数保持不变至少一秒钟,之后,通过用户在标签上单击,获得递增。一种实现方法是,将另一个参数添加到类型为 DateTime 的 loop 函数,它将保存一种成功单击的最后时间。当事件发生在循环内,我们可以算出当前时间和最后时间的差,只有这个差超过限额时,才增加计数。


    有一个更简单的方式来实现这一目标。在第 13 章中,我们讨论过Async.Sleep 方法,它可以停止工作流一定的时间。如果我们循环函数中的某处使用它,它会在响应下一个事件之前,暂停一秒钟,这正是我们想要的。我们要做的就是在运行递归的最后一行之前,添加下面一行:


 


do! Async.Sleep(1000)


 


    使用 Observable 模块的函数,是很难做到的。如果你好奇,可以在本书网站的源代码中,找到使用 Observable 函数的解决方案,大约有 8 行,理解起来有点难。这个示例的控制流仍然是相当简单的。在下一节中,我们将探讨一个更复杂的示例,更好地演示了异步工作流用于图形用户界面编程的能力。


 


 


------------------


[2] Async.AwaitObservable 基元最终可能会成为 F# 的核心或 F# PowerPack 库的一部分。
原创粉丝点击