Puzzled by copying of func params in GCD queues

HI,

I’m just learning Swift and was looking at Grand Central Dispatch (GCD) for some CPU intensive tasks. Here are (I believe) all the relevant bits of code I’m puzzled by:

Code Block
// global declarations
var digits     = [Int]()   // where the square's digits reside
var perm_q     = [[Int]]()  // where we accumulate permutations to check later
Let perm_q_max   = 1000     // how many permutations we queue before checking them
var enq_count    = 0      // how many times we've called check_squares
var N = 3 // size of N x N square
let work_q = DispatchQueue.global()  // returns a global, concurrent dispatch queue
let work_g = DispatchGroup()      // returns a group we put all our work into
// func which is enqueued onto a GCD queue
func check_squares( cnt: Int, perm_ary: [[Int]]) { ... }
// func which enqueues check_squares() onto global GCD queue
func permute( k: Int, ary: inout [Int]) {
  if k == 1 {
   
   perm_q.append( ary)  // queue up this permutation for later magic checking
   // if we've queued up enough permutations, then dispatch check_squares()
   if ( perm_q.count >= perm_q_max) {
      enq_count += 1
     // --> let p: [[Int]] = perm_q  // make a local copy
    work_q.async( group: work_g) {    // assign work all to one group
     check_squares( cnt: enq_count,   // check to see if any square is magic
             perm_ary: p)
    }
    perm_q = [[Int]]()          // clear out previous permutations
   }
  }
 else { ... }
}
// main
// Create a dispatch queue onto which we'll put the square-checking tasks.
digits = Array( 1 ... ( N * N))  // fill digits with digits 1...N^2
permute( k: digits.count, ary: &digits)  // creates permutations and checks for magic squares

The problem I’m having is that unless I uncomment the line just above work_q.async() in permute(), when check_squares() starts, ary has zero elements when I expect it to have 1,000 elements. Right after I enqueue check_squares() to GCD on the global async queue, I do perm_q = [[Int]]() which empties array perm_q to be ready to collect the next 1,000 elements.

I’m guessing there is a race condition between starting check_squares() and emptying perm_q, and the emptying occurs before check_squares() starts, but I’m puzzled as to why this race occurs. I understood that the call to check_squares() would make a copy of perm_q, and I thought this copy would happen with the call to check_squares().

One explanation I thought of is that the copy of perm_q into check_squares()’s param ary doesn’t happen until GCD starts to execute check_squres(). By the time this happens, perm_q has been emptied. Is that when the copy of perm_q into ary happens and not when check_squares() is enqueued? Making the local copy of global var perm_q into var p local to permute() and passing p to check_squares() during the enqueue makes local var p stick around since the reference from check_squares() in the queue keeps array p from disappearing even after permute() exits. Does this sound right?

Other than making the local copy of perm_q into p, is there a preferred method of handing this?

Thanks, Eric

Other than making the local copy of perm_q into p

Swift Arrays are not thread-safe, so having a private copy for each concurrent task seems to be the right way.
Why do you exclude copying?

Swift Arrays are not thread-safe

I want to expand on OOPer’s answer a little. Swift has a general rule about concurrent access called The Law of Exclusivity. This states that mutation must be exclusive. You can’t write a value while some other code is writing that same value, or write a value while some other code is reading it. In single-threaded code Swift enforces this using a combination of compile-time and runtime checks. However, Swift is unable to enforce this in multi-threaded code. If you’re writing multi-threaded code, you are responsible for following The Law™.

Note If you’d like to know more about The Law of Exclusivity, check out the Swift Ownership Manifesto.

The weird array behaviour you’re seeing is a result of this problem. Consider your handling of perm_q. On line 28 you clear it out — that is, you write it — and on line 26 you read it (assuming you’ve disabled the copy into p). However, line 26 is running on a secondary thread, so there’s a possibility these two accesses might run at the same time, and that’s not allowed.

My general advice on this front is to avoid sharing mutable state between multiple threads. The more mutable state you share, the more likely it is you’ll end up breaking the rules. If you’re lucky, the resulting problems will be clear to see, like they are here. In most cases, however, the problems will only crop up rarely and that makes things much harder to debug.

Your weapon of choice here should be the thread sanitiser. That will quickly uncover problems like this. See Diagnosing Memory, Thread, and Crash Issues Early.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
OOPer, I don't exclude copying, but not knowing if there was a better way, it seemed kind of like a hack, so I wanted to see if there was a more standard way to deal with this problem. Thanks for the reply. -Eric
Quinn, thanks for your informative reply. I will follow the pointers you provided. I thought it must be something like this, and I'm glad to see there is some formality behind it. I shall endeavor to follow The Law™. -Eric
Puzzled by copying of func params in GCD queues
 
 
Q