PAYATHON 2026

Swift 并发写入数组时如何处理 captured var 警告?

数据小许

结论

警告是因为并发闭包修改了捕获的可变数组 pixels。即使代码按行分块,实际写入的索引互不重叠,编译器也无法仅凭这段代码证明并发访问是安全的。可以让每个任务生成自己的局部像素块,再由任务组外的代码按顺序合并,这样既能保留并行计算,也不需要多个任务直接修改同一个数组。

下面的写法需要放在 async 函数中。示例沿用原来的分块思路。ColorFunction2D 是否能安全地跨任务传递(例如是否满足 Sendable),取决于它的定义。

用局部结果替代并发写共享数组

func makePixels(
    color2D: ColorFunction2D,
    width: Int,
    height: Int,
    chunkHeight: Int = 64
) async -> [UInt32] {
    let chunkCount = (height + chunkHeight - 1) / chunkHeight

    let chunks = await withTaskGroup(of: (Int, [UInt32]).self) { group in
        for chunkIndex in 0..<chunkCount {
            group.addTask {
                let yStart = chunkIndex * chunkHeight
                let yEnd = min(yStart + chunkHeight, height)
                var chunkPixels: [UInt32] = []
                chunkPixels.reserveCapacity((yEnd - yStart) * width)

                for y in yStart..<yEnd {
                    for x in 0..<width {
                        let color = color2D.color(at: Float(x), Float(y)).premultiplied_abgr
                        chunkPixels.append(color)
                    }
                }

                return (chunkIndex, chunkPixels)
            }
        }

        var collected: [(Int, [UInt32])] = []
        for await chunk in group {
            collected.append(chunk)
        }
        return collected.sorted { $0.0 < $1.0 }
    }

    var pixels: [UInt32] = []
    pixels.reserveCapacity(width * height)
    for (_, chunkPixels) in chunks {
        pixels.append(contentsOf: chunkPixels)
    }
    return pixels
}

每个任务只修改自己创建的 chunkPixels。任务组外的代码负责收集和合并,因此不会有多个任务同时修改同一个数组。任务完成顺序不固定,所以合并前要排序,确保像素顺序与图像行顺序一致。

生成图像时,可以继续使用合并后的 pixels:

let pixels = await makePixels(color2D: color2D, width: width, height: height)

let colorSpace = CGColorSpaceCreateDeviceRGB()
let context = pixels.withUnsafeBufferPointer { buffer in
    CGContext(
        data: UnsafeMutableRawPointer(mutating: buffer.baseAddress!),
        width: width,
        height: height,
        bitsPerComponent: 8,
        bytesPerRow: width * 4,
        space: colorSpace,
        bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
    )
}

let cgImage = context!.makeImage()!

这段图像创建代码假设宽、高都大于零,此时缓冲区才有 baseAddress。如果尺寸可能为零,应先处理空尺寸,避免强制解包空指针或无效上下文。

如果仍要并发写一个缓冲区

也可以改用不安全指针访问数组,并明确保证各任务写入的内存区间互不重叠。这样一来,正确性由代码作者负责;即使索引设计正确,编译器也无法自动验证访问安全。还要确保缓冲区在所有并发写入结束前保持有效,并避免其他地方同时读写它。

不建议只为消除诊断而屏蔽并发检查。如果项目允许串行处理,去掉并发执行也能解决这类共享写入问题,但会失去并行计算带来的收益。

注意事项

  • 分块区间要覆盖每个像素且彼此不重叠;合并时按行或块的原始顺序排列。
  • Swift 的严格并发检查还可能要求 ColorFunction2D 及其使用的状态满足相应的并发安全约束。这里没有提供该类型的定义,因此无法判断它是否符合要求。
  • CGContext 的创建参数、像素格式和 alpha 配置应与实际缓冲区布局一致。示例沿用问题中的设置,未验证颜色通道是否符合预期。

备注:内容仅供参考。