Copy-on-Write, value types and concurrentPerform

It seems struct can be mixed with concurrentPerform to get expected behavior, see the follow two code snippet:

var data: [Int] = [0, 0, 0, 0]
DispatchQueue.concurrentPerform(iterations: 4) { i in
  data[i] = i
}
print(data)

or

var data: (Int, Int, Int, Int) = (0, 0, 0, 0)
DispatchQueue.concurrentPerform(iterations: 4) { i in
  switch i {
  case 0:
    data.0 = i
  case 1:
    data.1 = i
  case 2:
    data.2 = i
  case 3:
    data.3 = i
  default:
    break
  }
}
print(data)

However, if you implemented copy-on-write naively with isKnownUniquelyReferenced, you can see in the block execution, that will return false in some cases and cause the underlying buffer copied.

Question:

Does above code has data race or not? If it does, how to write the code to not have data race?
If it is legit and shouldn't have data race, how should cow types implement the same behavior properly?

Per exclusivity rule, mutating data[i] or data.i requires write-access to the entire data. Both examples ought to fail when the mutation overlaps (not sure if it does, though).

I'm almost certain you need to drop down to Unsafe APIs.

var data: [Int] = [0, 0, 0, 0]
data.withUnsafeMutableBufferPointer { buffer in
    DispatchQueue.concurrentPerform(iterations: 4) { i in
        buffer[i] = i
    }
}
print(data)

You also need to provide similar functionality in your custom storage.

The value provided to the closure of withUnsafeMutableBufferPointer is not meant to be captured and used elsewhere, though. In this example buffer is captured by the closure passed to concurrentPerform which is executed some time in the future. You would need to either start with a unsafe buffer that you manage the lifetime of yourself, or ask for the unsafe mutable buffer pointer within the concurrent block. Though I bet that this may have issues, too.

concurrentPerform waits until all executions are finished, doesn't it? It wouldn't outlive the closure in that case.

The exclusivity rule explanation makes sense. But what's "safe" way to use concurrentPerform in this case? In many cases, when you use concurrentPerform, you almost certainly going to index into some underlying storage and drop to unsafePointer seems to be an overkill if there are guarantees can be maintained.

No, it’s like DispatchQueue.async. It doesn’t block.

It does say in the documentation, though:

The dispatch queue executes the submitted block the specified number of times and waits for all iterations to complete before returning.

And I ran a quick test on Xcode:

print("Starting")
DispatchQueue.concurrentPerform(iterations: 10) { i in
    sleep(UInt32(i))
    print("\(i)")
}
print("Done")

It seems wait until the end.

I couldn't figure it out either. They most likely won't overlap as you said, but I don't think Swift has enough power to express that exclusivity to the compiler.

1 Like

Whoa! Thanks for pointing that out.

1 Like

Right. Thanks for clarify. Another thing is, the execution block of concurrentPerform doesn't contain @escaping keyword, thus, how the "copy" is done for the said struct (or array) is still unclear to me (intuitively yes, but since this block won't be copied, practically & conceptually, unclear to me).

This is a good place to use withUnsafeBuffer:

var data: [Int] = Array(repeating: .min, count: 20)
data.withUnsafeMutableBufferPointer { buffer in
  DispatchQueue.concurrentPerform(iterations: buffer.count) { buffer[$0] = $0 }
}
print(data)

This version complies with the thread sanitizer. I would expect to see evidence of a data race at some point with the version from the first post.

Terms of Service

Privacy Policy

Cookie Policy