F # async cancel не работает - застрял на console.readline

Я запускаю простое приложение для чата с f #. В чате, когда один из пользователей вводит «выход», я хочу, чтобы оба клиента закончили чат. В настоящее время я работаю в консоли, поэтому чтение и запись блокируются, но я использую класс для обертывания консоли, чтобы не было проблем с асинхронностью.

(В следующем коде sendUI и reciveUI являются асинхронными функциями, которые отправляют и получают сообщения по сети)

type IConnection =
    abstract Send : string -> Async<bool>
    abstract Recieve : unit -> Async<string>
    abstract Connected : bool
    abstract Close : unit -> unit

type IOutput =
    abstract ClearLine : unit -> unit
    abstract ReadLine : ?erase:bool -> string
    abstract WriteLine : string -> unit

let sendUI (outputer:#IOutput) (tcpConn: #IConnection) () =
    async {
        if not tcpConn.Connected then return false
        else
        let message = outputer.ReadLine(true)
        try 
            match message with
            | "exit" -> do! tcpConn.Send "exit" |> Async.Ignore
                        return false
            | _      -> if message.Trim() <> "" 
                        then do! message.Trim() |> tcpConn.Send |> Async.Ignore
                        outputer.WriteLine("me: " + message)
                        return true
        with
        | e -> outputer.WriteLine("log: " + e.Message)
               return false
    }

let recieveUI (outputer:#IOutput) (tcpConn: #IConnection) () =
    async {
        if not tcpConn.Connected then return false
        else
        try
            let! response = tcpConn.Recieve()
            match response with
            | "exit" -> return false
            | _ -> outputer.WriteLine("other: " + response)
                   return true
        with
        | e -> outputer.WriteLine("error: " + e.Message)
               return false
    }

let rec loop (cancel:CancellationTokenSource) f =
    async {
        match! f() with
        | false -> cancel.Cancel(true)
        | true -> do! loop cancel f
    }

let messaging recieve send (outputer: #IOutput) (tcpConn:#IConnection) =
    printfn "write: exit to exit"
    use cancelSrc = new CancellationTokenSource()
    let task =
        [ recieve outputer tcpConn
          send    outputer tcpConn ]
        |> List.map (loop cancelSrc)
        |> Async.Parallel
        |> Async.Ignore
    try
        Async.RunSynchronously (computation=task, cancellationToken=cancelSrc.Token)
    with
    | :? OperationCanceledException ->
        tcpConn.Close()

let exampleReceive = 
    { new IConnection with
          member this.Connected = true
          member this.Recieve() = async { do! Async.Sleep 1000
                                          return "exit" }
          member this.Send(arg1) = async { return true }
          member this.Close() = ()
    }

let exampleOutputer =
    { new IOutput with
          member this.ClearLine() = raise (System.NotImplementedException())
          member this.ReadLine(erase) = Console.ReadLine()
          member this.WriteLine(arg) = Console.WriteLine(arg) }

[<EntryPoint>]
let main args =
    messaging recieveUI sendUI exampleOutputer exampleReceive
    0

(Я обернул консоль объектом, чтобы на экране не появлялись странности: outputer)

Когда я получаю «выход» по проводу, я возвращаю false, и поэтому вызовы цикла отменяются, поэтому он также должен остановить асинхронные вычисления отправки сообщений.

Однако когда я это делаю, sendUI застревает:

async {
    //do stuff
    let message = Console.ReadLine() //BLOCKS! doesn't cancel
    //do stuff
}

Одно из исправлений - каким-то образом сделать Console.ReadLine () асинхронным, однако простой async {return ...} не работает.

Я также пробовал запускать его как задачу и вызывать Async.AwaitTask, но это тоже не работает!

Я читал, что можно использовать Async.FromContinuations, но я не мог понять, как его использовать (и то, что я пробовал, не помогло ...)

Небольшая помощь?

РЕДАКТИРОВАТЬ

Причина, по которой это просто не работает, заключается в том, что отмена асинхронных вычислений работает. Они проверяют, следует ли отменить, когда доходит до let! / Do! / Return! и т.д., поэтому приведенные выше решения не работают.

РЕДАКТИРОВАТЬ 2

Добавлен образец исполняемого кода

Асинхронная передача данных с помощью sendBeacon в JavaScript
Асинхронная передача данных с помощью sendBeacon в JavaScript
В современных веб-приложениях отправка данных из JavaScript на стороне клиента на сервер является распространенной задачей. Одним из популярных...
1
0
137
1

Ответы 1

Вы можете обернуть Console.ReadLine в отдельный async, а затем вызвать его с помощью Async.RunSynchronously и CancellationToken. Это позволит вам отменить операцию блокировки, потому что она не будет находиться в том же потоке, что и сама консоль.

open System
open System.Threading

type ITcpConnection =
    abstract member Send: string -> unit

let readLineAsync cancellation =
    async {
        try
            return Some <| Async.RunSynchronously(async { return Console.ReadLine() }, cancellationToken = cancellation)
        with | _ ->
            return None
    }

let receiveUI cancellation (tcpConnection: ITcpConnection) =
    let rec loop () =
        async {
            let! message = readLineAsync cancellation
            match message with
            | Some msg -> msg |> tcpConnection.Send
            | None -> printfn "Chat Session Ended"
            return! loop ()
        }
    loop () |> Async.Start

Я попробовал, но ничего не вышло. Причина все та же - отмена действует только при достижении let! / Do! / Return! и т.д., если вы выполняете асинхронное вычисление и не используете! он никогда не достигнет точки, в которой он проверит, нужно ли отменить

tjw 24.09.2018 18:48

@tjw Можете ли вы опубликовать полный, работоспособный пример? Я сам попробовал это в интерактивном сеансе, и это сработало.

Aaron M. Eshbach 24.09.2018 19:13

Другие вопросы по теме