同时填充数组

4

我在Swift 5中遇到了并发和数组问题。为了复现问题,我简化了代码如下:

import Dispatch

let group = DispatchGroup()
let queue = DispatchQueue(
  label: "Concurrent threads",
  qos: .userInitiated,
  attributes: .concurrent
)

let threadCount = 4
let size = 1_000
var pixels = [SIMD3<Float>](
  repeating: .init(repeating: 0),
  count: threadCount*size
)

for thread in 0..<threadCount {
  queue.async(group: group) {
    for number in thread*size ..< (thread+1)*size {
      let floating = Float(number)
      pixels[number] = SIMD3<Float>(floating, floating, floating)
    }
  }
}

print("waiting")
group.wait()
print("Finished")

当我在使用Xcode 10.2 beta 4 (10P107d)进行调试时,它总是崩溃,并显示错误信息:

Multithread(15095,0x700008d63000) malloc: *** error for object 0x104812200: pointer being freed was not allocated
Multithread(15095,0x700008d63000) malloc: *** set a breakpoint in malloc_error_break to debug

我感觉这是编译器的一个 bug,因为当我以发布模式运行代码时,它可以正常运行。或者是我的做法有误?


我会非常怀疑在多个线程同时写入“pixels”数组。 - JeremyP
确实,启用线程检测器并运行该代码会立即触发“警告:ThreadSanitizer:Swift访问竞争”。但我无法重现malloc错误。 - Martin R
好的,但我没有打算同时访问数组中的任何项。那么我该如何解决这个问题呢?不同的线程访问数组的不同非重叠部分。我只想同时填充数组的不同部分。 - Damiaan Dufaux
你能解释一下为什么我的代码会生成一个释放未分配指针的程序吗? - Damiaan Dufaux
1个回答

10

在数组内部有指针,它们绝对可以在您脚下更改。这不是原始内存。

数组不是线程安全的。数组是值类型,这意味着它们支持以线程安全的方式写入副本(因此您可以自由地将数组传递到另一个线程,并且如果在那里进行复制,那就没问题),但您不能在多个线程上更改同一个数组。数组不是C缓冲区。它不能保证具有连续的内存。它甚至不能保证分配内存。数组可以选择在内部存储“我当前全部为零”作为特殊状态,并返回每个下标的0。(它不会,但是它被允许。)

对于这个特定的问题,通常会使用类似vDSP_vramp的vDSP方法,但我理解这只是一个例子,可能没有解决问题的vDSP方法。尽管如此,我仍然会专注于加速/SIMD方法,而不是派遣到队列。

但是,如果您要调度到队列,则需要使用UnsafeMutableBuffer来控制内存(并确保内存确实存在):

pixels.withUnsafeMutableBufferPointer { pixelsPtr in
    DispatchQueue.concurrentPerform(iterations: threadCount) { thread in
        for number in thread*size ..< (thread+1)*size {
            let floating = Float(number)
            pixelsPtr[number] = SIMD3(floating, floating, floating)
        }
    }
}

"Unsafe"表示现在需要您自己确保所有访问都是合法的,并且您没有创建竞争条件。

请注意这里使用了.concurrentPerform。正如@user3441734所提醒的,一旦.withUnsafeMutablePointer完成,pixelsPtr不能保证有效。 .concurrentPerform保证不会返回,直到所有块都完成,因此指针得到保证是有效的。

这也可以使用DispatchGroup完成,但.wait需要在withUnsafeMutableBufferPointer内部。


1
抱歉,SIMD3 是我打错了的一个字。 - Damiaan Dufaux
1
在 withUnsafeMutableBufferPointer 中,您实际上不允许修改指针(该方法在返回时验证指针仍具有相同的值)。指针是 inout 的,因此您可以对其调用可变方法(例如 sort),请参见 https://forums.swift.org/t/se-0245-add-an-array-initializer-with-access-to-uninitialized-storage/21469/10。 - Martin R
2
@user3441734,我不太确定您在这里所说的依赖于当前实现是什么意思。withUnsafeMutableBufferPointer旨在(并记录为)直接访问数组的可变连续存储。这都是定义良好的行为。您有哪些实现方面的顾虑?将大型数组连接在一起可能非常昂贵;如果size为100万(考虑处理21600x21600 BlueMarble图像),我不确定为什么您会说“具有最小的性能损失”。 - Rob Napier
1
我不知道你所说的“不同的副本”是什么意思。withUnsafeMutableBufferPointer只被调用了一次,它会确保一次性地获得连续的存储(实际上,几乎肯定已经是连续的存储)。唯一复制的是指针,而不是缓冲区。 - Rob Napier
1
@RobNapier 我理解你的观点,我所说的是(来自苹果文档):“传递给 body 的指针仅在执行 withUnsafeMutableBufferPointer(_: )期间有效。不要存储或返回指针以供以后使用。”换句话说,在执行异步闭包时,不能保证它仍然有效(或将来实现)。这就是我的观点。 - user3441734
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接