需要有关Async和fsi的帮助

时间:2010-04-15 21:39:11

标签: f# sequence asynchronous f#-interactive

我想编写一些运行一系列F#脚本(.fsx)的代码。问题是我可以拥有数百个脚本,如果我这样做的话:

let shellExecute program args =
    let startInfo = new ProcessStartInfo()
    do startInfo.FileName        <- program
    do startInfo.Arguments       <- args
    do startInfo.UseShellExecute <- true
    do startInfo.WindowStyle     <- ProcessWindowStyle.Hidden

    //do printfn "%s" startInfo.Arguments 
    let proc = Process.Start(startInfo)
    ()

scripts
|> Seq.iter (shellExecute "fsi")

可能对我的2GB系统施加太多压力。无论如何,我想按批次运行脚本n,这似乎也是学习Async的一个很好的练习(我想这是要走的路)。

我已经开始为此编写一些代码,但不幸的是它不起作用:

open System.Diagnostics

let p = shellExecute "fsi" @"C:\Users\Stringer\foo.fsx"

async {
    let! exit = Async.AwaitEvent p.Exited
    do printfn "process has exited"
}
|> Async.StartImmediate

foo.fsx只是一个hello world脚本。 解决这个问题最常用的方法是什么?

我还想弄清楚是否可以检索每个执行脚本的返回码,如果没有,找到另一种方法。谢谢!

编辑:

非常感谢您的见解和链接!我学到了很多东西。 我只想添加一些代码来使用Async.Parallel并行运行批处理,就像Tomas建议的那样。如果我的cut功能有更好的实施,请发表评论。

module Seq =
  /// Returns a sequence of sequences of N elements from the source sequence.
  /// If the length of the source sequence is not a multiple
  /// of N, last element of the returned sequence will have a length
  /// included between 1 and N-1.
  let cut (count : int) (source : seq<´T>) = 
    let rec aux s length = seq {
      if (length < count) then yield s
      else
        yield Seq.take count s
        if (length <> count) then
          yield! aux (Seq.skip count s) (length - count)
      }
    aux source (Seq.length source)

let batchCount = 2
let filesPerBatch =
  let q = (scripts.Length / batchCount)
  q + if scripts.Length % batchCount = 0 then 0 else 1

let batchs =
  scripts
  |> Seq.cut filesPerBatch
  |> Seq.map Seq.toList
  |> Seq.map loop

Async.RunSynchronously (Async.Parallel batchs) |> ignore

EDIT2:

所以我有一些麻烦让Tomas的守卫代码工作。我想必须在f方法中调用AddHandler函数,否则我们永远会松开事件......这是代码:

module Event =
  let guard f (e:IEvent<´Del, ´Args>) = 
    let e = Event.map id e
    { new IEvent<´Args> with 
        member this.AddHandler(d) = e.AddHandler(d); f() //must call f here!
        member this.RemoveHandler(d) = e.RemoveHandler(d); f()
        member this.Subscribe(observer) = 
          let rm = e.Subscribe(observer) in f(); rm }

有趣的事情(正如Tomas所提到的)是,当进程终止时,看起来Exited事件存储在某处,即使进程尚未以EnableRaisingEvents设置为true开始。 当此属性最终设置为true时,将触发该事件。

由于我不确定这是官方规范(也有点偏执),我找到了另一种解决方案,即在guard函数中启动该过程,因此我们确保代码可以正常工作在任何情况下:

let createStartInfo program args =
  new ProcessStartInfo
    (FileName = program, Arguments = args, UseShellExecute = false,
     WindowStyle = ProcessWindowStyle.Normal, 
     RedirectStandardOutput = true)

let createProcess info =
  let p = new Process()
  do p.StartInfo           <- info
  do p.EnableRaisingEvents <- true
  p

let rec loop scripts = async { 
  match scripts with 
  | [] -> printfn "FINISHED"
  | script::scripts ->
    let args = sprintf "\"%s\"" script
    let p = createStartInfo "notepad" args |> createProcess
    let! exit =
      p.Exited 
      |> Event.guard (fun () -> p.Start() |> ignore)
      |> Async.AwaitEvent
    let output = p.StandardOutput.ReadToEnd()
    do printfn "\nPROCESSED: %s, CODE: %d, OUTPUT: %A"script p.ExitCode output
    return! loop scripts 
  }

注意我已经用 notepad.exe 替换了 fsi.exe ,这样我就可以在调试器中逐步重放不同的场景,并自行控制进程的退出

5 个答案:

答案 0 :(得分:6)

我做了一些实验,这里有一种方法可以解决我在帖子下面的评论和Joel的回答中讨论的问题(我认为目前不起作用,但可以修复)。

认为 Process的规范是,在我们将Exited属性设置为EnableRaisingEvents后,它可以触发true事件(和即使在我们设置属性之前已完成该过程,也会触发事件)。为了正确处理这种情况,我们需要在将处理程序附加到Exited事件后启用事件的引发。

这是一个问题,因为如果我们使用AwaitEvent,它将阻止工作流,直到事件触发。从工作流程中调用AwaitEvent后我们无法执行任何操作(如果我们在调用AwaitEvent之前设置属性,那么我们就会参加比赛....)。 Vladimir's approach是正确的,但我认为有一种更简单的方法可以解决这个问题。

我将创建一个函数Event.guard来获取一个事件并返回一个事件,这允许我们指定一些函数将在处理程序附加到事件之后执行。这意味着如果我们在此函数中执行某些操作(进而触发事件),则将处理该事件。

要将其用于此处讨论的问题,我们需要更改原始解决方案,如下所示。首先,shellExecute函数不能设置EnableRaisingEvents属性(否则,我们可能会丢失事件!)。其次,等待代码应如下所示:

let rec loop scripts = async { 
  match scripts with 
  | [] -> printf "FINISHED"
  | script::scripts ->
    let p = shellExecute fsi script 
    let! exit = 
      p.Exited 
        |> Event.guard (fun () -> p.EnableRaisingEvents <- true)
        |> Async.AwaitEvent
    let output = p.StandardOutput.ReadToEnd()
    return! loop scripts  } 

请注意使用Event.guard功能。粗略地说,在工作流将处理程序附加到p.Exited事件之后,提供的lambda函数将运行(并将启用事件的引发)。但是,我们已经将处理程序附加到事件中,因此如果这会立即导致事件,我们就没事了!

实施(适用于EventObservable)如下所示:

module Event =
  let guard f (e:IEvent<'Del, 'Args>) = 
    let e = Event.map id e
    { new IEvent<'Args> with 
        member x.AddHandler(d) = e.AddHandler(d)
        member x.RemoveHandler(d) = e.RemoveHandler(d); f()
        member x.Subscribe(observer) = 
          let rm = e.Subscribe(observer) in f(); rm }

module Observable =
  let guard f (e:IObservable<'Args>) = 
    { new IObservable<'Args> with 
        member x.Subscribe(observer) = 
          let rm = e.Subscribe(observer) in f(); rm }

好的是这段代码非常简单。

答案 1 :(得分:5)

您的方法看起来很棒,我真的很喜欢使用AwaitEvent将流程执行嵌入异步工作流程的想法!

它不起作用的可能原因是您需要将EnableRisingEvents的{​​{1}}属性设置为Process,如果您希望它能够触发true事件(不要问我为什么要这样做,这对我来说听起来很傻!)无论如何,我在测试时对你的代码做了一些其他修改,所以这里有一个对我有用的版本:

Exited

最重要的是,代码现在将open System open System.Diagnostics let shellExecute program args = // Configure process to redirect output (so that we can read it) let startInfo = new ProcessStartInfo (FileName = program, Arguments = args, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden, RedirectStandardOutput = true) // Start the process // Note: We must enable rising events explicitly here! Process.Start(startInfo, EnableRaisingEvents = true) 设置为EnableRaisingEvents。我还更改了代码以使用一种语法,在构造它时指定对象的属性(使代码更简洁)并更改了一些属性,以便我可以读取输出(true

现在,我们可以使用RedirectStandardOutput方法等待进程完成。我假设AwaitEvent包含fsi.exe的路径,fsi是FSX脚本的列表。如果要按顺序运行它们,可以使用使用递归实现的循环:

scripts

当然,您也可以并行运行流程(或者例如并行运行2组)。为此,您可以使用let rec loop scripts = async { match scripts with | [] -> printf "FINISHED" | script::scripts -> // Start the proces in background let p = shellExecute fsi script // Wait until the process completes let! exit = Async.AwaitEvent p.Exited // Read the output produced by the process, the exit code // is available in the `ExitCode` property of `Process` let output = p.StandardOutput.ReadToEnd() printfn "\nPROCESSED: %s, CODE: %d\n%A" script p.ExitCode output // Process the rest of the scripts return! loop scripts } // This starts the workflow on background thread, so that we can // do other things in the meantime. You need to add `ReadLine`, so that // the console application doesn't quit immedeiately loop scripts |> Async.Start Console.ReadLine() |> ignore (以通常的方式)。

无论如何,这是一个非常好的例子,在我迄今尚未见过它的区域中使用异步工作流。非常有趣: - )

答案 2 :(得分:3)

在回答Tomas的回答时,这是否可以解决启动流程所涉及的竞争条件,然后订阅其退出事件?

type Process with
    static member AsyncStart psi =
        let proc = new Process(StartInfo = psi, EnableRaisingEvents = true)
        let asyncExit = Async.AwaitEvent proc.Exited
        async {
            proc.Start() |> ignore
            let! args = asyncExit
            return proc
        }

除非我弄错了,否则这会在开始流程之前订阅该事件,并将其全部打包为Async<Process>结果。

这将允许您重写其余代码,如下所示:

let shellExecute program args = 
  // Configure process to redirect output (so that we can read it)
  let startInfo = 
    new ProcessStartInfo(FileName = program, Arguments = args, 
        UseShellExecute = false,
        WindowStyle = ProcessWindowStyle.Hidden, 
        RedirectStandardOutput = true)

  // Start the process
  Process.AsyncStart(startInfo)

let fsi = "PATH TO FSI.EXE"

let rec loop scripts = async { 
    match scripts with 
    | [] -> printf "FINISHED"
    | script::scripts ->
        // Start the proces in background
        use! p = shellExecute fsi script 
        // Read the output produced by the process, the exit code
        // is available in the `ExitCode` property of `Process`
        let output = p.StandardOutput.ReadToEnd()
        printfn "\nPROCESSED: %s, CODE: %d\n%A" script p.ExitCode output
        // Process the rest of the scripts
        return! loop scripts 
} 

如果能胜任这项工作,那么担心的代码肯定要比弗拉基米尔Async.GetSubject少得多。

答案 3 :(得分:1)

邮箱处理器怎么样?

答案 4 :(得分:1)

可以从blogpost简化主题版本。而不是返回模仿事件,getSubject可以返回工作流程。

结果工作流本身是具有两种状态的状态机 1.事件尚未触发:所有待处理的侦听器都应该注册 2.已设置值,立即提供听众 在代码中它将如下所示:

type SubjectState<'T> = Listen of ('T -> unit) list | Value of 'T

getSubject实现很简单

let getSubject (e : IEvent<_, _>) = 
    let state = ref (Listen [])
    let switchState v = 
        let listeners = 
            lock state (fun () ->
                match !state with
                | Listen ls -> 
                    state := Value v 
                    ls
                | _ -> failwith "Value is set twice"
            )
        for l in listeners do l v

    Async.StartWithContinuations(
        Async.AwaitEvent e,
        switchState,
        ignore,
        ignore
    )

Async.FromContinuations(fun (cont, _, _) ->
    let ok, v = lock state (fun () ->
        match !state with
        | Listen ls ->
            state := Listen (cont::ls)
            false, Unchecked.defaultof<_>
        | Value v ->
            true, v
        )
    if ok then cont v
    )