GlobalQueue 会导致 iOS 内存泄漏吗?

Does GlobalQueue cause memory leaks in iOS?

这是一个非常简单的演示:

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
            
        for i in 0..<500000 {
            DispatchQueue.global().async {
                print(i)
            }
        }
    }
}

当我从模拟器中 运行 这个演示时,内存使用量上升到 ~17MB,最后下降到 ~15MB。但是,如果我注释掉 dispatch code 并且只保留 print() 行,则内存使用量仅为 ~10MB。每当我更改循环计数时,增加量都会有所不同。

是否存在内存泄漏?我尝试了 Leaks 但没有找到任何东西。

内存已用不是内存泄漏

某些 OS 服务会产生一定的开销。我记得回答过一个使用 WebView 的人提出的类似问题。有全局缓存。只有 code 必须从磁盘调入内存(这对 WebKit 来说很重要),一旦代码调入,它就不太可能被调出。

我最近没有查看 libdispatch 源代码,但 GCD 维护着一个或多个线程池,用于执行您入队的块。这些线程中的每一个都有一个堆栈。 macOS 上的默认线程堆栈大小为 8MB。我不知道 iOS 的默认线程堆栈大小,但它肯定有一个(我敢打赌它有 8MB)。一旦 GCD 创建了这些线程,为什么要关闭它们?尤其是当您向 OS 表明您将快速排队 500K 操作时?

OS 正在以内存使用为代价针对 performance/speed 进行优化。你无法控制它。这不是“泄漏”,它是已分配但没有实时引用的内存。这个记忆肯定有对它的实时引用,它们只是不在你的控制之下。如果您想要更多(尽管有所不同)了解内存使用情况,请查看 vmmap 命令。它可以(并且将会)向您展示可能会让您感到惊讶的事情。

在查看内存使用情况时,必须 运行 循环几次才能得出“泄漏”的结论。它可能只是缓存。

在这种特殊情况下,您可能会在第一次迭代后看到内存增长,但不会在后续迭代中继续增长。如果这是一个真正的泄漏,post-peak 基线将继续爬升。但事实并非如此。请注意,第二个峰后的基线与第一个峰后的基线基本相同。


顺便说一句,这里的内存特性是线程爆炸的结果(你应该始终避免)。考虑:

for i in 0 ..< 500_000 {
    DispatchQueue.global().async {
        print(i)
    }
}

将 50 万个工作项分派到一个队列,该队列一次只能支持 64 个工作线程。即“thread-explosion”超出工作线程池。

您应该改为执行以下操作,这会限制 concurrentPerform 的并发程度:

DispatchQueue.global().async {
    DispatchQueue.concurrentPerform(iterations: 500_000) { i in
        print(i)
    }
}

这实现了同样的事情,但将并发程度限制为可用 CPU 核心的数量。这避免了许多问题(特别是它避免了耗尽可能导致其他系统死锁的有限工作池线程),但它也避免了内存中的峰值。 (见上图,后两个 concurrentPerform 过程后都没有峰值。)

因此,虽然 thread-explosion 场景实际上并没有泄漏,但由于内存峰值和潜在的死锁风险,应该不惜一切代价避免这种情况。