First of all consider using Swift Atomics, so you won't need a custom class. Or consider using an actor
for your data structure, as suggested in the comment.
A couple of implementation notes:
Your access to values
is not thread-safe. I would recommend to make them private, and either create a thread-safe iterator, or provide a copy of the array. Otherwise you may get an exception (I forget the exact wording, but it says something like "array was mutated while being accessed")
Each operation in your atomic class is atomic, but their sequential calls are not. For example in your test you do this:
arrayA.append(item: i)
if let last = arrayA.last
While each operation is atomic here, you are not guaranteed that arrayA.last
will be the same element you appended just in the line above (because other thread may have appended something else after it)
So if you do need to access array and then modify it (or vice versa), consider adding an atomic operation that does such sequence, or some generic approach for sequential calls, for example described here
I also agree that it makes more sense to make Atomic to be a class
or actor
, not struct
. But BTW the problem above is not solved even if you switch your data structure to be an actor
.
Number of DispatchQueue
s in the system is limited. So depending on how you are planning to use this atomic list (or how many of them you are planning to have), you may need a static
queue.
Thirdly: XCTestExpectation
(surprise-surprise) is not thread safe and hence cannot be used as a reliable method of checking thread safety of your atomic class. There are well documented cases where it caused various issues (this for example), and I can confirm from my experience as well that it may indeed cause something like "9999 is not equal to 10000".
So what I usually suggest to do for testing such a component is
Break it to several tests: a test for each operation, and test for each combination of operations that makes sense. In your case it means 3 tests: for last
, for append
and for the both operations working together. I am disregarding the access to array itself here, but if you make it thread-safe, you will need then a test for array, as well as possibly additional tests for "read and modify array concurrently" (but that depends on how you implement your array access)
Use DispatchQueue.concurrentPerform
as a way to do operations concurrently. Why? because it guarantees to complete after all iterations are done, and hence you don't need XCTestExpectation. BTW: the Swift Atomics I mentioned above also uses this method in its tests, and to demo the thread safety.
Also you don't need that many iterations, all you really want is to load all threads available during the test. Usually the test only has about 10 threads. Fine, you can run 100 iterations just to be sure, but 10000 is quite excessive, and doesn't add any value to your test.
So a simple set of tests would look like this:
func testLast() {
// Given
var a = AtomicList<Int>()
var i = Int.random(in: 1...100)
a.append(i)
// When: several threads reading the value of `last` concurrently
DispatchQueue.concurrentPerform(iterations: 100) { _ in
// Then: the read value is correct (and no crash happened)
XCTAssertEqual(a.last, i)
}
}
func testAppend() {
// Given
var a = AtomicList<Int>()
let iterations = 100
// When: several threads appending concurrently
// We cannot control the order of values, so we will test after all iterations are done.
DispatchQueue.concurrentPerform(iterations: iterations) { i in
a.append(i)
}
// Then
XCTAssertEqual(a.count, iterations)
// I would also sort an array and check its elements, but that's optional
}
func testAppendAndLast() {
// Given
var a = AtomicList<Int>()
let iterations = 100
// When
DispatchQueue.concurrentPerform(iterations: iterations) { i in
// We cannot be sure what last will be when we are reading it,
// But we can be sure that if we read current last, then append, then read again, they should be different.
let lastBefore = a.last
a.append(i)
let lastAfter = a.last
XCTAssertNotEqual(lastBefore, lastAfter)
}
}