如何确保在继续之前启动Async.StartChild?

时间:2018-04-09 13:12:44

标签: asynchronous f# async-await

我正在尝试等待超时的事件。我在函数startAwaitEventWithTimeout后面抽象了这个。目前我的代码看起来像这样(包括一些调试输出消息):

let startAwaitEventWithTimeout timeoutMs event =
  async {
    Console.WriteLine("Starting AwaitEvent in eventAwaiter")
    let! eventWaiter = Async.StartChild(Async.AwaitEvent event, timeoutMs)
    try
      Console.WriteLine("Awaiting event in eventAwaiter")
      let! res = eventWaiter
      return Ok res
    with :? TimeoutException ->
      return Error ()
  } |> Async.StartChild

这是一个测试:

let testEvent = Event<string>()

[<EntryPoint>]
let run _ =
  async {
    Console.WriteLine("Starting event awaiter in main")
    let! eventAwaiter = testEvent.Publish |> startAwaitEventWithTimeout 1000

    Console.WriteLine("Triggering event")
    testEvent.Trigger "foo"
    Console.WriteLine("Awaiting event awaiter in main")
    let! result = eventAwaiter

    match result with
    | Ok str -> Console.WriteLine("ok: " + str)
    | Error () -> Console.WriteLine("TIMEOUT")
  } |> Async.RunSynchronously
  0

不幸的是,尽管一切都在等待着#34;据我所见,似乎run函数在Async.AwaitEvent有机会订阅事件之前继续触发事件。简而言之,这是我得到的输出:

Starting event awaiter in main
Starting AwaitEvent in eventAwaiter
Triggering event
Awaiting event awaiter in main
Awaiting event in eventAwaiter
TIMEOUT

以下是我的期望:

Starting event awaiter in main
Starting AwaitEvent in eventAwaiter
Awaiting event in eventAwaiter  <-- this is moved up
Triggering event
Awaiting event awaiter in main
ok foo

我可以通过添加例如在调用do! Async.Sleep 100和触发事件之间startAwaitEventWithTimeout,但当然这不太理想。

我做错了什么,有没有办法可以确保在触发事件之前调用AwaitEvent

(旁注:我这样做是因为我们通过TCP调用远程进程,并且来自远程的所有通信都是通过事件完成的。)

2 个答案:

答案 0 :(得分:2)

可能我缺少一些要求,但您的代码可以使用continuation轻松重构,并且错误由其自身修复。

let testEvent = Event<unit>()

let run _ =
  let ts = new CancellationTokenSource(TimeSpan.FromSeconds(float 1))
  let rc r =  Console.WriteLine("ok")
  let ec _ =  Console.WriteLine("exception")
  let cc _ =  Console.WriteLine("cancelled")
  Async.StartWithContinuations((Async.AwaitEvent testEvent.Publish), rc , ec,  cc, ts.Token  )
  testEvent.Trigger()
run()

编辑:如果您有使用异步工作流的特定要求,可以使用TPL中的TaskCompletionSource进行转换。

let registerListener  timeout event= 
  let tcs = TaskCompletionSource()
  let ts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout))
  let er _ =  tcs.SetResult (Error())
  Async.StartWithContinuations(Async.AwaitEvent event, tcs.SetResult << Ok , er , er , ts.Token)
  Async.AwaitTask tcs.Task

let run _ =
  let testEvent = Event<int>()
  async {
       let listener = registerListener (float 1) testEvent.Publish
       testEvent.Trigger 2
       let! ta  = listener 
       match ta with
         | Ok n -> printfn "ok: %d" n
         | Error () -> printfn "error"
  } |> Async.RunSynchronously

run()

请注意,尽管比产生/等待多个子计算更容易理解,但大部分代码仍然是样板,我相信设置简单超时值必须有更简单的解决方案。

答案 1 :(得分:0)

我认为您不会遇到竞争条件,因为您在子计算开始之前始终触发事件。让我们改变设置 - 就像你为测试做的那样 - 在开火前加入延迟。

fn select_lines<'a>(pattern: &String, lines: &'a Vec<String>) -> Vec<&'a String> {
    let mut selected_lines: Vec<&'a String> = Vec::new();

    for line in lines {
        if line.contains(pattern) {
            selected_lines.push(&line);
        }
    }

    selected_lines
}

如果射击延迟等于超时,则会出现竞争条件。