F# 代理与超时
在我上一篇文章中,我提供了一段非常简单的F#代理控制台应用程序的代码。每次您运行控制台应用程序时,只需要输入一行文本,它会产生一条新消息,然后把这条消息发布到一个F#代理的消息队列。该代理是一个MailboxProcessor类的一个实例。MailboxProcessor是在F#核心库中Control命名空间中的一个类。正如MailboxProcessor名称中所暗示的,代理就是您可以发送消息给它,它运行一些代码响应您发送的消息。这个代理运行的代码可以很方便的用您传进去的一个Lambda表达式来表示。
通常情况下,您传进去的Lambda表达式都需要遵循一个共同的模式,就是反复地从一个消息队列中取信息的循环,每次一个。在每次迭代循环的时候,你会调用Receive方法。
下面是我帖出来的一段实例代码。在这段代码中,我们创建了一个代理和它的消息处理循环,正如我前面所描述的那样。另外,我们运行了一个循环,它的作用是每次从控制台读取一行代码,然后把每行代码发送给代理。如果您运行这个程序,当您每次输入一些文字的时候,这些文字作为消息被接收。除了基本的信息处理之外,这个例子也显示了如何通过使用返回管道(一个AsyncReplyChannel类型的对象)把消息返回给调用者。在这个简单例子中发生的唯一的处理过程是,如果接收到的消息是“Stop”然后发送一个特殊的答复,其他情况,则连同我们接收到的消息和它的ID一起发送回去。
module MailboxProcessorTest2 =
open System
type Message = string * AsyncReplyChannel<string>
let formatString ="Message number {0} wasreceived. Message contents: {1}"
let agent =MailboxProcessor<Message>.Start(funinbox->
let rec loop n =
async {
let! (message, replyChannel) = inbox.Receive();
if(message = "Stop")then
replyChannel.Reply( "Stopping.")
else
replyChannel.Reply(String.Format(formatString, n, message))
do! loop (n + 1)
}
loop 0)
printfn "MailboxProcessor Test"
printfn "Entersome text and hit ENTER to submit a message."
printfn "Type'Stop' to terminate."
let rec loop() =
printf ">"
let input = Console.ReadLine()
let reply = agent.PostAndReply(funreplyChannel-> input, replyChannel)
if(reply <> "Stopping.")then
printfn "Reply:%s" reply
loop()
else
()
loop() printfn "Pressenter to continue."
Console.ReadLine() |> ignore在消息处理循环中,其他有趣的事情是async{…}.异步编程是F#的优势之一。MailboxProcessor是为异步工作而设计的。所以,例如,lambda表达式的返回类型是Async<unit>(一个异步的工作项目)。这让我们在监听消息的同时不因为等待下个消息而阻塞程序运行。使用let!代替let,使用do!代替do来表示这是一个异步操作。其基本思路是:执行程序,直到碰到let!或do!,然后,如果没有返回值或者还没运行结束,程序可以在其他地方继续执行。然后,当返回值是可用的或执行结束时,程序就从该行代码继续执行。在后台,使用回调函数,将let!或do!后面的代码打包到回调函数。显然,所有的处理是相当复杂的,所以人们喜欢的设计是可以像自己所期望地那样随心所欲地写异步执行代码,在代码里用一些叹号(!),F#异步工作流就会让它变成异步执行。
F#的异步功能是它最吸引人的特点之一。事实上,异步功能也正在C#和VB中顺利地实现着,您可以尝试使用在2010年秋季PDC会议上发布的CTP版本。尽管它看起来有点像一个黑匣子。在我发布的这一系列Blog中我希望去探索asynchronous编程模式并阐明这个黑匣子。另外,我们希望拥有一个能思考它如何运作的思维模式。如果它所做的是每次执行一个新的逻辑的“程序流”就启动一个后台线程的话,这将是一个问题。在这个例子中,我们使用了两个循环,一个是读取输入,另外一个是处理消息。您可能会认为,有两个线程在并行处理,因为有两个循环同时发生了。让我们通过打印线程ID来测试这个假设。
let printThreadId note =
// Append the Thread ID
printfn "%d :%s" System.Threading.Thread.CurrentThread.ManagedThreadId note
添加一些调用printThreadId方法的代码,使用”MailboxProcessor”或者”Console”作为参数。如果你运行这个,你将会发现它确实是对的,MailboxProcessor循环运行在不同线程上从的控制台UI线程上读取数据。我发现当运行这个的时候,其实有两个后台线程从消息队列中读取信息。下面是添加这些变化后的代码:
module MailboxProcessorTest2 =
open System
type Message = string * AsyncReplyChannel<string>
let formatString = "Message number {0} wasreceived. Message contents: {1}"
let printThreadId note =
// Append theThread ID
printfn "%d: %s" System.Threading.Thread.CurrentThread.ManagedThreadId note
let agent =MailboxProcessor<Message>.Start(funinbox->
let recloop n =
async {
let! (message, replyChannel) = inbox.Receive();
printThreadId "mailboxProcessor"
if(message = "Stop" ) then
replyChannel.Reply( "Stopping.")
else
replyChannel.Reply(String.Format(formatString, n, message))
do! loop (n + 1)
}
loop 0)
printfn "MailboxProcessor Test"
printfn "Entersome text and hit ENTER to submit a message."
printfn "Type'Stop' to terminate."
let recloop() =
printf ">"
let input = Console.ReadLine()
printThreadId( "Consoleloop" )
let reply = agent.PostAndReply(funreplyChannel -> input, replyChannel)
if(reply <> "Stopping.")then
printfn "Reply:%s" reply
loop()
else
()
loop()
printfn "Pressenter to continue."
Console.ReadLine() |> ignore
下面是我的部分输出:
Mailbox ProcessorTest
Enter some text andhit ENTER to submit a message.
Type 'Stop' toterminate.
> test
1 : Console loop
4 : mailboxProcessor
Reply: Messagenumber 0 was received. Message contents: test
> another loop
1 : Console loop
3 : mailboxProcessor
Reply: Messagenumber 1 was received. Message contents: another loop
> another loop
1 : Console loop
4 : mailboxProcessor
Reply: Messagenumber 2 was received. Message contents: another loop
> testing
1 : Console loop
3 : mailboxProcessor
Reply: Messagenumber 3 was received. Message contents: testing
> testing
1 : Console loop
4 : mailboxProcessor
Reply: Messagenumber 4 was received. Message contents: testing
假设我现在想添加一些超时条件进去,那么,如果一段时间都没有新的控制台输入的话,这个程序将提示如下信息:发生超时,跳至下一个消息ID。接收端把以毫秒为单位的整型值——超时作为一个可选参数。我设置了一个10秒的超时条件,当这个超时条件被满足时,将会抛出一个超时异常。最初,我尝试着把try/with结构放到调用PostAndReply的代码的外面来捕获这个异常。但是,这个异常却从未被捕获! 我仅仅能在产生此异常的线程中捕获它, 所以这个异常处理器应该放置在MailboxProcessor<Message> 循环中。我也不得不小心在超时的情况下开始下一个循环迭代。 哦, 我被如此顺畅的F# 异步处理工作流震惊了, 我甚至不知道我是在不同的线程上运行的。下面是超时处理的实现:
module MailboxProcessorTest3 =
open System
type Message = string * AsyncReplyChannel<string>
let formatString = "Message number {0} was received. Message contents: {1}"
let agent = MailboxProcessor<Message>.Start(funinbox ->
let rec loop n=
async {
try
let! (message, replyChannel) = inbox.Receive(10000);
if (message = "Stop" ) then
replyChannel.Reply( "Stop")
else
replyChannel.Reply(String.Format(formatString, n, message))
do! loop (n + 1)
with
| :? TimeoutException ->
printfn "The mailbox processor timedout."
do! loop (n + 1)
}
loop(0))
printfn "MailboxProcessor Test"
printfn "Entersome text and hit ENTER to submit a message."
printfn "Type'Stop' to terminate."
let recloop() =
printf ">"
let input = Console.ReadLine()
let reply = agent.PostAndReply(funreplyChannel-> input, replyChannel)
if (reply <> "Stop")then
printfn "Reply:%s" reply
loop()
else
()
loop() printfn "Pressenter to continue."
Console.ReadLine() |> ignore现在,当你运行这个程序时,如果你等待足够长的时间,那么这个超时信息将会被打印出来, 但是新的信息处理工作仍在继续,而且这个信息的ID会跳过一个。 下面是一个例子:
Mailbox ProcessorTest
Enter some text andhit ENTER to submit a message.
Type 'Stop' toterminate.
> hello
Reply: Messagenumber 0 was received. Message contents: hello
> hello?
Reply: Messagenumber 1 was received. Message contents: hello?
> The mailboxprocessor timed out.
testing
Reply: Messagenumber 3 was received. Message contents: testing
> Stop
Press enter tocontinue.
当然,有些时候你会希望这个异常被传播到发布此超时消息的线程上。这里,你可以通过Error 事件来做到这点。这个Error事件需要一个Lamdba表达式来处理异常。下面是一个示例——代理抛出超时异常,Error事件处理这个异常:
module MailboxProcessorWithError =
open System
type Message = string
let agent = MailboxProcessor<Message>.Start(funinbox ->
let rec loop n=
async {
let! message = inbox.Receive(10000);
printfn "Message number %d. Messagecontents: %s"n message
do! loop (n+1)
}
loop 0)
agent.Error.Add(fun exn ->
match exn with
| :? System.TimeoutException as exn ->printfn "Theagent timed out."
printfn "PressEnter to close the program."
Console.ReadLine() |> ignore
exit(1)
| _ -> printfn "Unknown exception.")
printfn "MailboxProcessor Test"
printfn "Typesome text and press Enter to submit a message."
while true do
Console.ReadLine() |> agent.Post
这些代码的意图在于, 我想创建一个能够允许我从命令行来启动一些后台作业的代理。这个超时条件并不是真的必要的,但是它确实能够帮助我们弄清楚发生在消息循环中的异常行为。 假如我想从后台开始一些作业, 我希望一次操作就能启动多个作业,这样一来我就不必等到PostAndReply有了返回值后才能提交下一个消息了。在下一个版本中, 我将使用PostAndAsyncReply,这样一来,消息处理循环方法和输入方法都将采用异步处理机制。
原文链接:http://blogs.msdn.com/b/gordonhogenson/archive/2011/01/10/f-agents-with-timeouts.aspx