3. Metal设备与命令队列
3.1 获取 MTLDevice:你的第一行 Metal 代码
任何 Metal 应用的第一步,都是拿到一个 MTLDevice 对象。它代表 GPU 的抽象,所有命令、缓冲区、纹理都从这里诞生。
用苹果的 API,一行就够了:
guard let device = MTLCreateSystemDefaultDevice() else {
fatalError("Metal 不可用 — 这年头不可能")
}
但这里有个坑。我曾在一个项目中,用模拟器跑这段代码,结果直接 crash。为什么?模拟器不支持 Metal。所以生产环境一定要加判断,别假设所有设备都有 GPU。
MTLCreateSystemDefaultDevice() 在模拟器中返回 nil。你可以用 MTLCopyAllDevices() 枚举所有设备,或者直接在真机测试。
如果你有多 GPU 的 Mac(比如部分 Intel Mac 有集显+独显),可以用 MTLCopyAllDevices() 拿到所有设备,根据功耗或性能选择:
let devices = MTLCopyAllDevices()
for device in devices {
print("\(device.name), lowPower: \(device.isLowPower)")
}
| 获取方式 | 适用场景 | 注意点 |
|---|---|---|
MTLCreateSystemDefaultDevice() | 绝大多数 iOS / macOS 应用 | 模拟器返回 nil,需 fallback |
MTLCopyAllDevices() | 多 GPU Mac、选择低功耗或高性能 | 在 iOS 上只返回一个 |
3.2 MTLCommandQueue:GPU 的任务管线
有了 device,下一步就是创建 Command Queue。它的职责很简单:排队提交命令缓冲区(CommandBuffer)给 GPU 执行。
我个人习惯每个线程只创建一条 queue,避免浪费资源:
guard let commandQueue = device.makeCommandQueue() else {
fatalError("创建 CommandQueue 失败")
}
一个 queue 可以同时提交多个 command buffer,苹果保证顺序执行(FIFO)。如果你需要并行编码多组命令,可以用 makeCommandQueue(maxCommandBufferCount:) 指定最大缓冲数。不过说实话,默认值大多数情况都够用。
3.3 多线程安全:谁动了我 queue?
Metal 的 Command Queue 是 线程安全 的——这是我说的第一个好消息。你可以多个线程同时调用 makeCommandBuffer(),它内部有锁。
但 CommandBuffer 和 Encoder 不是线程安全的!每个线程必须用自己的 buffer 和 encoder。怎么搞?我推荐一个「生产者-消费者」模型:
- 一个全局 queue(主线程创建)。
- N 个工作线程,每线程创建自己的 command buffer,编码自己的 blit / compute 命令。
- 编码完成后,通过同一 queue 提交 buffer。
看代码:
// 全局 queue,线程安全
let queue = device.makeCommandQueue()!
// 工作线程
DispatchQueue.global(qos: .userInitiated).async {
let buffer = queue.makeCommandBuffer()!
let encoder = buffer.makeComputeCommandEncoder()!
// ... 编码工作 ...
encoder.endEncoding()
buffer.commit()
buffer.waitUntilCompleted()
}
嗯,这里要注意——千万别把 encoder 扔到其他线程去用。我在一个老项目里犯过这个错:后台线程用同一个 encoder 同时编码,结果 Metal 直接崩溃。后来加了个 @synchronized 勉强不崩,但性能惨不忍睹。正确做法是在每个线程内部创建独立的 encoder。
3.4 内存与生命周期管理
MTLDevice 和 MTLCommandQueue 在对象存活期间一直有效,不用关心释放。但 CommandBuffer 和 Encoder 是临时对象,提交后建议调用 waitUntilCompleted 或通过回调跟踪完成状态。
你想想看,如果你不断创建 buffer 又不等待,内存会持续增长。Metal 有自动回收机制,但最好显式管理。我的经验是:
- 编码循环中,每次用完 encoder 立即
endEncoding()。 - 频繁调用的路径,重用 buffer 而不是每次都新建。
- 用
MTLCommandBufferHandler回调检测 GPU 完成,及时释放资源。
下面这张图展示了整个交互关系:
3.5 避坑清单
- 不要在模拟器上依赖 Metal — 真机才能测试完整 GPU 功能。
- 不要跨线程共享 Encoder — 每个线程独立创建。
- 不要频繁创建新 Queue — 3~5 条足矣,超过则性能下降。
- 别忘了等待 GPU 完成 — 特别是你要复用资源时。