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
相反方向(注意:最好在客户端呈现此消息,并在线上发送类型 request
和 response
的值,这将使您的流量保持较低水平,更重要的是,将允许透明地连接不同的客户端)。
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_protocol
、play_protocol
等。但您可能还会注意到,这些协议有一些交叉点。要处理此问题,您可以使用子类型化和多态变体。
我还在写一个简单的游戏服务器。由于
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
相反方向(注意:最好在客户端呈现此消息,并在线上发送类型 request
和 response
的值,这将使您的流量保持较低水平,更重要的是,将允许透明地连接不同的客户端)。
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_protocol
、play_protocol
等。但您可能还会注意到,这些协议有一些交叉点。要处理此问题,您可以使用子类型化和多态变体。