Ocaml Lwl_mvar.take 不阻塞线程

Ocaml Lwl_mvar.take does not block thread

我还在写一个简单的游戏服务器。由于 的一条建议,我实现了 mvar 支持,希望它能在不包含至少 2 个玩家的情况下阻塞线程。但它不会等到我把任何数据放在那里。它总是 return 睡觉 Lwt.t。 首先,这里我们接受连接并提供玩家输入START开始寻找伙伴:

let waiting_players = 
    Lwt_mvar.create_empty();;

let rec make_ready player = 
    player >>= fun cli ->
        send_to_client player "Type in START";
        let answer = read_from_client player in 
            answer >>= fun str ->
            match str with
                |"START" -> 
                    let ready_client =  cli in  
                    send_to_client player "Waiting for opponent";
                    Lwt_mvar.put waiting_players ready_client;
                | _ -> 
                    send_to_client player "Unknown command. try again";
                    make_ready player

let handle_income () =
    let in_conection = Lwt_unix.accept sock in 
    in_conection >>= fun (cli, addr) ->
    let player = Lwt.return cli in
    send_to_client player "Welcome to the server. To start game type in START and press Enter";
    make_ready player;;

    val make_ready : Lwt_unix.file_descr Lwt.t -> unit Lwt.t = <fun>
    val handle_income : unit -> unit Lwt.t = <fun>

似乎没问题,但是当我调用 Lwt_mvar.take waiting_players 时,它总是 returns 一些值,甚至之前没有放置任何值,并且线程没有被阻塞。这种奇怪的(对我来说)行为在示例中更好地看到:

# let bucket = Lwt_mvar.create_empty ();;
val bucket : '_a Lwt_mvar.t = <abstr>

# let apple = Lwt_mvar.take bucket;;
val apple : '_a Lwt.t = <abstr>

# Lwt.state apple;;
- : '_a Lwt.state = Sleep

如果 "blocking" 表示 return 正是这样的睡眠对象,请告诉。以及如何制作一个循环,returning only "ready" objects 的最佳方式?使用 Lwt.is_sleeping 是个好主意吗?非常感谢。

找到方法了。

let rec form_pairs () = 
let player1 = Lwt_mvar.take waiting_players in 
    player1 >>= fun descriptor1 ->
let player2 = Lwt_mvar.take waiting_players in 
    player2 >>= fun descriptor2->
Lwt_io.printl "Pairs formed";
Lwt.return (descriptor1, descriptor2);
form_pairs ();;

您的方法几乎没有问题,代码中存在一些错误。因此,我将首先强调后者,然后提出并论证另一种方法。

问题

第 1 期

看起来你的 send_to_client return 是一个 unit Lwt.t 类型的值。如果您只是通过用 ; 终止表达式来忽略它,那么它意味着 "don't wait until the message is send and move forward"。通常这不是您想要的。因此,您需要等待 unit Lwt.t 线程完成,绑定到它的 return 值。

第 2 期

通常在 Lwt 编程中,函数接受立即类型的值(即未包装到 Lwt.t 中的值)和 returns 延迟线程(即 [=24= 类型的值) ]).当然,这通常是没有人阻止你做一些不同的事情。但尽量坚持 "immediate inputs, delayed output" 模式。

第 3 期

使用工具。使用 ocp-indent 缩进您的代码,这将有助于提高可读性。此外,看起来您不使用编译器并且正在玩顶级游戏。通常这是一个坏主意,尤其是对于系统编程。使用 ocamlbuild 编译和 运行 您的代码:

ocamlbuild game.native --

游戏

与 Python 或其他弱类型系统语言相比,OCaml 编程具有不同的理念。在 OCaml 中,应该从设计类型和签名开始,然后再填写实现。当然这是理想化,在现实生活中还会有一个迭代提炼的过程,但大体的做法还是一样的。从类型开始。

所以,首先,让我们定义一个 player 类型。这是微不足道的,但有改进的余地。

open Lwt

type player = {
  fd : Lwt_unix.file_descr
}

接下来,让我们使用类型系统来帮助我们,了解我们的游戏初始化问题。你需要让两名玩家准备好并愿意玩你的游戏。这意味着,你有三个连续的状态:

  • Nobody 准备就绪
  • One player 准备就绪
  • Both (player1, player2) 准备就绪

实际上,一旦你达到第三种状态,你就可以开始游戏了,你不需要那种状态,所以我们最终只有两个选择:

type stage =
  | Nobody
  | One of player

我们可以在这里使用 player option 类型,因为它与我们的选择同构。但让我们更明确一点,使用我们自己的 stage 类型。它将使我们的模型更加受限和适合。

下一步是定义客户端和服务器之间的交互协议。我们将使用名称 request 表示从服务器到客户端的消息,response 表示相反方向的消息。

type request =
  | Init
  | Wait
  | Unknown_command
  | Bye 

type response =
  | Start
  | Quit

这个协议是抽象的,因为它不包含任何具体的表示——基于它你可以构建不同的表示,例如,GUI 界面,或支持不同语言的文本聊天。

但是让我们模拟一个最简单的具体实现,它使用文本命令:

let response_of_string msg =
  match String.trim (String.uppercase msg) with
  | "START" -> Some Start
  | "QUIT" -> Some Quit
  | _ -> None

相反方向(注意:最好在客户端呈现此消息,并在线上发送类型 requestresponse 的值,这将使您的流量保持较低水平,更重要的是,将允许透明地连接不同的客户端)。

let string_of_request = function
  | Init -> "Welcome to a game server.
    Please, type 
    - `start' to start game;
    - `quit' to finish session"
  | Wait -> "Please wait for another player to join the game"
  | Unknown_command -> "Don't understand this"
  | Bye -> "Thank you, see you later!"

下一步是为 Io 定义接口。该模块负责客户端和服务器之间的交互。请注意我们如何通过抽象隐藏所有细节,例如使用套接字或字符串。

module Io : sig
  val send : player -> request -> unit Lwt.t
  val recv : player -> response option Lwt.t 
end = struct
  let send dst msg = return_unit
  let recv dst = return None
end 

现在,我们可以定义我们的 Game 模块。一开始它会有两个不同的自动机:

  • init 初始化两个玩家之间的游戏;
  • play玩一次游戏,我们有两个受害者。

让我们在 OCaml 中明确地说:

module Game : sig

  (** [play a b] play a game between player [a] and player [b] *) 
  val play : player -> player -> unit Lwt.t

  (** [init next_player] waits until two players are ready to play.
      TODO: Describe a grammar that is recognized by this automaton. *)
  val init : (unit -> player Lwt.t) -> (player * player) Lwt.t
end = struct
  let play a b = return_unit

  let init next_player =
    let rec process stage player = 
      Io.send player Init >>= fun () -> 
      Io.recv player >>= function
      | None ->
        Io.send player Unknown_command >>= fun () ->
        process stage player
      | Some Quit ->
        Io.send player Bye >>= fun () -> 
        next_player () >>= process stage
      | Some Start -> match stage with
        | One a -> return (a,player)
        | Nobody -> 
          Io.send player Wait >>= fun () ->
          next_player () >>= process (One player) in
    next_player () >>= process Nobody
end

现在我们可以写出 main 函数,它将所有东西粘合在一起:

let main server_sock = 
  let next_player () =
    Lwt_unix.accept server_sock >>=
    fun (fd,_) -> return {fd} in
  Game.init next_player >>= fun (a,b) -> 
  Game.play a b

当您继续使用这种方法时,您稍后可能会注意到,游戏的不同有限状态机定义了不同的语言(即协议)。因此,您最终可能不会使用一种协议,而是为每个 FSM 使用一种特定的协议,例如 init_protocolplay_protocol 等。但您可能还会注意到,这些协议有一些交叉点。要处理此问题,您可以使用子类型化和多态变体。