给出以下代理,这是一个简单的缓存机制:
type CacheMsg<'a,'b> = Add of 'a * 'b | ForceFlush
type CacheAgent<'a, 'b when 'a : comparison>(size:int, flushCont:Map<'a, 'b> -> unit) =
let agent = MailboxProcessor.Start(fun inbox ->
let rec loop (cache : Map<'a, 'b>) = async {
let inline flush() =
flushCont cache
loop Map.empty
if cache.Count > size then return! flush()
let! msg = inbox.Receive()
match msg with
| Add (key, value) ->
if cache.ContainsKey key then
return! loop cache
else return! loop (cache.Add(key, value))
| ForceFlush -> return! flush() }
loop Map.empty)
member x.AddIfNotExists key value = Add(key,value) |> agent.Post
member x.ForceFlush() = agent.Post ForceFlush
此代理将继续占用内存(似乎在调用flushCont
时内存未释放。)
给出相同的代码,但稍有改动:
type CacheMsg<'a,'b> = Add of 'a * 'b | ForceFlush
type CacheAgent<'a, 'b when 'a : comparison>(size:int, flushCont:Map<'a, 'b> -> unit) =
let agent = MailboxProcessor.Start(fun inbox ->
let rec loop (cache : Map<'a, 'b>) = async {
let inline flush() =
flushCont cache
loop Map.empty
let! msg = inbox.Receive()
match msg with
| Add (key, value) ->
if cache.ContainsKey key then
return! loop cache
else
let newCache = cache.Add(key, value)
if newCache.Count > size then
return! flush()
else return! loop (cache.Add(key, value))
| ForceFlush -> return! flush() }
loop Map.empty)
member x.AddIfNotExists key value = Add(key,value) |> agent.Post
member x.ForceFlush() = agent.Post ForceFlush
我已将决定何时刷新的表达式移动到联合案例Add
中。这导致内存按预期释放。
第一种方法有什么问题,因为它会泄漏内存?
答案 0 :(得分:12)
第一个版本不是tail recursive。
这不是尾递归,因为这个表达式不是函数中的最后一个表达式:
if cache.Count > size then return! flush()
在表达式之后,您调用
let! msg = inbox.Receive()
所以flush()
电话不是最后发生的事情。在flush
中隐式的递归调用完成后,执行将需要返回到下一个表达式,在该表达式中调用inbox.Receive()
。这意味着上下文必须将先前的调用保留在堆栈上,因为递归不在尾部位置:还有更多工作要做。
在第二个示例中,对flush
和loop
的所有来电都处于尾部位置。
如果你来自C#背景,你会倾向于认为return! flush()
退出了这个功能,但事实并非如此。唯一的原因
if cache.Count > size then return! flush()
即使在没有相应else
分支的情况下编译也是because the expression returns unit
。这意味着then
分支内的代码并不真正退出函数 - 它只是在分支中执行工作(在本例中为flush()
),然后继续执行后续表达式。