f# 和递归中的内存使用

f# and memmory usage in recursive

最近我想学习更多关于内存分析的知识,所以我有一个简单的应用程序可以从 UDP 接收消息。现在我使用标准 Visual Studio 2019 内存分析器 https://memprofiler.com/.

从外观上看,我在这个应用程序中的内存使用量很小但不断增加,但我不知道我是否在做某事(内存分析的术语对我来说很新,所以我大部分时间都这样做不要打结我在看什么)。

我的问题和分析器的问题数据向我展示了这一点

快照拍摄间隔大约10分钟(但程序运行一个小时,我每10分钟拍摄一次快照,所有进程都稳定只是这部分在增加)

因此,如果我理解正确,问题出在过程 $UdpListener.listenerWatcher@76-5 中,它是代码的一部分,如下所示:

member this.Start (udpReceiver : UdpClient) (onMessageReceived : byte array -> IBus -> unit) = 
    async {
        try
            let! receiveResult = udpReceiver.ReceiveAsync() |> Async.AwaitTask
            let receiveBytes = receiveResult.Buffer
            this.Logger.Debug 
                (sprintf "Received info %A from remote end poin Ip:%s Port:%i" receiveBytes (receiveResult.RemoteEndPoint.Address.ToString()) receiveResult.RemoteEndPoint.Port)
            onMessageReceived receiveBytes bus
        with 
            | :? ObjectDisposedException -> return ()
            | :? SocketException -> return ()
            | ex -> 
                match ex.InnerException :? ObjectDisposedException with 
                | true -> 
                    this.Logger.Warn "Receiver is disposed or socked is closed, probably receiver restart" 
                    return ()
                | _  ->
                    this.Logger.Fatal(sprintf "Fatal error while starting listening, exception %A" ex)
                    raise (ex)
        }

member this.Watcher (ip: IPAddress, port : int32) (onMessageReceived : byte array -> IBus -> unit) =
    let rec listenerWatcher (udpReceiver : UdpClient) = 
        async {
            try
                do! this.Start udpReceiver onMessageReceived
                return! listenerWatcher (udpReceiver) 
            with | ex ->
                this.Logger.Fatal (sprintf "Udp listener watcher has beed stoped %A" ex)
                return ()
        }    
    (ip, port) |> this.CreateUdpReceiver |> listenerWatcher 

UdpReceiver 在 watcher 启动时创建一次,它看起来像:

 member private this.CreateUdpReceiver (ip: IPAddress, port : int32) : UdpClient =
    try
        let endpoint = IPEndPoint (ip, port)
        let receivingClient = new UdpClient()
        receivingClient.Client.Bind endpoint
        receivingClient.Client.SetSocketOption (SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true)
        this.Logger.Debug (sprintf "Listener binded to Ip Adress:%s Port:%i" (endpoint.Address.ToString()) endpoint.Port)
        UdpMediator.Instance.InvokeAddUdpReceiver receivingClient
        receivingClient
    with | ex -> 
        this.Logger.Fatal(sprintf "Fatal error while stoping  udp clinet")
        raise (ex)

而 InvokeAddUpdReceiver 只是将创建的接收器添加到外部,因此我们可以在不需要或需要刷新时处理他。

所以基本上我无法确定这次泄漏的来源,对我来说它看起来不错,但我可能对某些事情视而不见。

我在您的代码中看到的一个潜在问题来源是在 listenerWatcher 函数中实现递归调用的方式。你有:

let rec listenerWatcher (udpReceiver : UdpClient) = async {
  try
    do! this.Start udpReceiver onMessageReceived
    return! listenerWatcher (udpReceiver) 
  with ex ->
    this.Logger.Fatal (sprintf "Udp listener watcher has beed stoped %A" ex) } 

这里的问题是使用 return! 的递归调用在 try .. with 块内。这可以防止异步运行时将其视为尾递归调用。它必须为异常处理分配一个处理程序,因此它为每个递归调用添加一个分配的对象。

您应该能够通过将异常处理移到递归循环之外来解决此问题:

let rec listenerWatcher (udpReceiver : UdpClient) = async {
  do! this.Start udpReceiver onMessageReceived
  return! listenerWatcher (udpReceiver) }

let startListenerWatcher udpReceiver = async {
  try 
    return! listenerWatcher udpReceiver
  with ex ->
    this.Logger.Fatal (sprintf "Udp listener watcher has beed stoped %A" ex) } 

或者,假设您不需要在递归调用中保留任何状态,您可以只使用 while 循环而不是递归:

let listenerWatcher (udpReceiver : UdpClient) = async {
  try 
    while true do 
      do! this.Start udpReceiver onMessageReceived
  with ex ->
    this.Logger.Fatal (sprintf "Udp listener watcher has beed stoped %A" ex) }