递归中的 f# 和内存使用

问题描述

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

从它的外观来看,我在这个应用程序中的内存使用量很小但不断增加,但我不知道我是否在做某事(内存分析的命名法对我来说很新,所以我通常不会打结我在看什么)。

所以归结为我的问题,分析器的问题数据向我展示了这一点

enter image description here

快照时间间隔大约为 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) }