F# 需要有关异步和fsi的帮助吗

F# 需要有关异步和fsi的帮助吗,f#,sequence,asynchronous,f#-interactive,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 <

我想编写一些运行一系列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")
fsx只是一个hello world脚本。 解决这个问题最惯用的方法是什么

我还想弄清楚,为每个正在执行的脚本检索一个返回代码是否可行,如果不可行,找到另一种方法。谢谢

编辑:

非常感谢您的见解和链接!我学到了很多。 我只想添加一些代码,以便使用Tomas建议的
Async.parallel
并行运行批处理。如果my
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
有趣的事情(正如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 
  }
让createStartInfo程序参数=
新流程StartInfo
(FileName=program,Arguments=args,UseShellExecute=false,
WindowsStyle=ProcessWindowsStyle.Normal,
重定向(标准输出=真)
让我们创建进程信息=
设p=newprocess()
p.StartInfo
让args=sprintf“\%s\”脚本
设p=createStartInfo“记事本”参数|>createProcess
让我来!出口=
p、 退出
|>Event.guard(fun()->p.Start()|>ignore)
|>异步事件
让output=p.StandardOutput.ReadToEnd()
是否打印fn“\n处理:%s,代码:%d,输出:%A”脚本p.ExitCode输出
回来!循环脚本
}

请注意,我已将fsi.exe替换为notepad.exe,这样我就可以在调试器中一步一步地重放不同的场景,并自己显式控制流程的退出。

您的方法对我来说太棒了,我非常喜欢使用
waitevent
将流程执行嵌入异步工作流的想法

它不起作用的可能原因是,如果您想让它触发退出的事件,您需要将
进程的
enableringEvents
属性设置为
true
(不要问我为什么要这样做,我觉得这很傻!),在测试您的代码时,我对其做了一些其他更改,因此这里有一个适合我的版本:

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
包含fsi.exe的路径,并且
scripts
是FSX脚本的列表。如果要按顺序运行它们,可以使用使用递归实现的循环:

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    
当然,您也可以并行运行这些进程(例如并行运行两组进程等),以使用
Async.parallel
(通常的方式)


无论如何,这是一个非常好的例子,在一个我还没有看到使用异步工作流的领域中使用异步工作流。非常有趣:-)

邮箱处理器怎么样

对于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
结果

这将允许您像这样重写其余的代码:

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 
} 
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  } 
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 }
type SubjectState<'T> = Listen of ('T -> unit) list | Value of 'T

如果这样做的话,那么比起Vladimir的
Async.GetSubject

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

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

这是一个问题,因为如果我们使用
AwaitEvent
,它将阻止工作流,直到事件触发。从工作流中调用
AwaitEvent
之后,我们无法执行任何操作(如果我们在调用
AwaitEvent
之前设置了属性,那么我们将获得一场竞赛……)。是正确的,但我认为有一个更简单的方法来处理这个问题

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

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

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 
} 
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  } 
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 }
type SubjectState<'T> = Listen of ('T -> unit) list | Value of 'T

好在这段代码非常简单。

可以简化blogpost中主题的版本。getSubject可以返回工作流,而不是返回对事件的模拟