Thread Security of Swift Array copy

Keywords: Swift

Thread Security of Swift Array copy

NSArray inherits from NSObject, belongs to object, and has copy method. Swift's Array is struct, and there is no copy method. Assigning an Array variable to another variable has the same memory address? Relevant multithreading security issues. This paper explores these two issues.

Memory address

Define test class es and struct s

class MyClass {
    
    var intArr = [Int]()
    var structArr = [MyStructElement]()
    var objectArr = [MyClassElement]()
}

struct MyStructElement {}

class MyClassElement {}

closure defining the output memory address

let memoryAddress: (Any) -> String = {
    guard let cVarArg = $0 as? CVarArg else { return "Can not find memory address" }
    return String(format: "%p", cVarArg)
}

Test Int array

private func testIntArr() {
    print(#function)
    
    let my = MyClass()
    for i in 0...10000 {
        my.intArr.append(i)
    }
    print("Before arr address:", memoryAddress(my.intArr))
        
    // Copy Array is NOT thread safe
    let arr = my.intArr // If move this into async closure, crash
    print("Temp   arr address:", memoryAddress(arr)) // Copy. Address different from my.intArr
    DispatchQueue.global().async {
        var sum = 0
        for i in arr {
            sum += i
        }
        print("Sum:", sum) // 0 + 1 + ... + 10000 = 50005000
    }
    
    my.intArr.removeAll()
    for _ in 0...10000 {
        my.intArr.append(0)
    }
    print("After  arr address:", memoryAddress(my.intArr)) // New address
}

Testing in view controller

override func viewDidLoad() {
    super.viewDidLoad()
    
    for _ in 0...1000 {
        testIntArr()
    }
}

Result

Int array has different memory addresses, and the assignment process produces a copy.

Testing struct array

private func testStructArr() {
    print(#function)
    
    let my = MyClass()
    for _ in 0...10000 {
        my.structArr.append(MyStructElement())
    }
    print("Before arr address:", memoryAddress(my.structArr))
    
    // Copy Array is NOT thread safe
    let arr = my.structArr // If move this into async closure, crash
    print("Temp   arr address:", memoryAddress(arr)) // Copy. Address different from my.structArr
    DispatchQueue.global().async {
        var sum = 0
        for _ in arr {
            sum += 1
        }
        print("Sum:", sum) // 10001
    }
        
    my.structArr.removeAll()
    for _ in 0...10000 {
        my.structArr.append(MyStructElement())
    }
    print("After  arr address:", memoryAddress(my.structArr)) // New address
}

Testing in view controller

override func viewDidLoad() {
    super.viewDidLoad()
    
    for _ in 0...1000 {
        testStructArr()
    }
}

Result

Struct array has different memory addresses, and the assignment process is copied.

Test Object array

private func testObjectArr() {
    print(#function)
    
    let my = MyClass()
    for _ in 0...10000 {
        my.objectArr.append(MyClassElement())
    }
    print("Before arr address:", memoryAddress(my.objectArr))
    
    // Copy Array is NOT thread safe
    let arr = my.objectArr // If move this into async closure, crash
    print("Temp   arr address:", memoryAddress(arr)) // Not copy. Same as my.objectArr
    DispatchQueue.global().async {
        var sum = 0
        for _ in arr {
            sum += 1
        }
        print("Sum:", sum) // 10001
    }
        
    my.objectArr.removeAll()
    for _ in 0...10000 {
        my.objectArr.append(MyClassElement())
    }
    print("After  arr address:", memoryAddress(my.objectArr)) // New address
}

Testing in view controller

override func viewDidLoad() {
    super.viewDidLoad()
    
    for _ in 0...1000 {
        testObjectArr()
    }
}

Result

An object array variable is assigned to another variable, and the memory addresses of the two variables are the same, that is, there is no copy. After the original array changes, the memory address changes, but does not affect the assigned variables.

Thread security issues

The above writing will not be wrong. If the assignment of array is written to async closure, an error will be reported. If you try several times, you will make different mistakes.

Int array error

DispatchQueue.global().async {
    let arr = my.intArr // There will be an error in assignment.
    var sum = 0
    for i in arr {
        sum += i
    }
    print("Sum:", sum)
}

Struct array error

DispatchQueue.global().async {
    let arr = my.structArr // There will be an error in assignment.
    var sum = 0
    for _ in arr {
        sum += 1
    }
    print("Sum:", sum)
}

Object array error

DispatchQueue.global().async {
    let arr = my.objectArr // There will be an error in assignment.
    var sum = 0
    for _ in arr {
        sum += 1
    }
    print("Sum:", sum)
}

For Int array and struct array, the assignment is copied, but this step should not be an atomic operation, so putting async closure will make a mistake. For object array, although the assignment process does not copy, to change the original array and keep the assigned object unchanged, it should also copy; that is, copy only when the array is updated. It is assumed that copying is not an atomic operation at this time, so putting async closure will make a mistake.

Whether Array's assignment process copies or not depends on the type of element in it. If the elements of array are Int, struct, etc., copy when assigned. If an array element is an object, it does not copy when it is assigned, and only copies when one of the array variables is updated after the assignment. Array's copy is thread insecure.

Posted by abkdesign on Wed, 20 Mar 2019 23:12:32 -0700