Is Swift's copy-on-write thread-safe?
Asked Answered
F

2

6

Making an array or dictionary a value type by definition, but then actually copying it only when one reference to it tries to modify it is a lovely idea, but it makes me wary in a multi-queued/threaded context. I need to know:

Is Swift's copy-on-write capability thread-safe? eg: If I create an array on one queue and pass it to another queue, is it safe for either queue to modify it while the other might be reading or modifying it? Since by definition the copy was made when the array reference was passed into the second queue, can we assume that the Swift engineers did the right thing and implemented copy-on-write in a queue-safe way?

I found this old discussion of this, which seems authoritative, but in both directions! https://developer.apple.com/forums/thread/53488

Some credible voices say it's thread-safe, others say it isn't. I imagine that this may be because in some early version of Swift it was not, while perhaps in Swift 5 it is. Does anyone here know for sure for Swift 5?

Here's some sample code to illustrate the issue:

func func1()
{
    var strings1: [String] = ["A", "B", "C"]
    var strings2: [String] = strings1   // array not actually copied
    queue.async()
    {
        strings2.append("D")
    }

    print(strings1[0])    // is this reference thread-safe?
    strings1.append("E")  // is this modification thread-safe?
}
French answered 14/8, 2020 at 15:11 Comment(5)
I didn't down-vote, but a recommendation: If you can base your question on practical code snippets rather than opinions in some debate elsewhere, you're less likely to receive down-votes.Centralism
Swift has always considered read/write and write/write races on the same variable to be undefined behavior. SE-0176 Enforce Exclusive Access to Memory.. as far as I recall, swift, originally, was not thread-safe intentionally by design.Seeker
I guess I don't understand something here: Why would someone down-vote a question? I asked a question that is not only a good one, the answer to is affects EVERYONE who is programming in Swift with multiple queues. No I did not include sample code. Why? Because it is trivial to imagine what I'm talking about, if you're at all familiar with multi-queued programming. Yes, I linked to another discussion. Is that a problem? I did this because it provided credible but contradictory answers to my question, which I found interesting. Why is anyone taking umbrage at this?French
Asperi, the question is not whether one can have two queues operating on exactly the same variable, as in the same global variable. Its whether they can operate on copies of the same value. The answer to this is simple if the value is a scalar type: Yes! It's also simple if you're using a language that truly makes copies of an array/dictionary value when you pass the value into a function or assign it to a new variable. The issue is that Swift says that CONCEPTUALLY such values ARE copied at assignment time, but in actuality the copy is only made later. Is this copy queue-safe?French
I know Rob is digging into the question much more deeply and will hopefully find something more definitive, but there's no way that COW isn't thread-safe. It's the whole point of value types in Swift. They'd be useless if it weren't thread-safe. I agree completely with Christopher that eskimo's comments are very surprising, and I wish I had a reference that said definitively that this obvious fact is true. (I fear it's so obvious that no one has felt the need to explicitly document it. This doesn't invalidate the question. It's a good question.)Jamilla
F
0

OK, since no one from Apple/Swift Inc is replying, I'll venture my best guess:

I imagine that when you have an Array value in swift, it's a reference to a reference to an NSArray or an NSMutableArray. (Yes, I know this is only true for class objects, but let's keep it simple here.) Without assigning a new value to your Array value, the lower-level reference can can be made to refer to a different NS-object by simple operations on it, such as appending or trimming. There is also a reference count attached to the underlying NS-object.

When you modify an Array, the first thing Swift does is check to see if the Swift reference is the only one to the underlying NS-object. If so, the NSArray is converted to an NSMutableArray if necessary and the modification takes place. If not, the NSArray is copied into an NSMutableArray, the modification takes place, and the lower-level Swift reference is changed to point to the new NS-object.

If this is indeed the process that Copy on Write follows, and if we can assume that the reference count mechanism, and the retain/release system is thread-safe, then I would say that Copy on Write IS thread-safe. Even if another thread is making the modification, as described above, since it's modifying a copy that IT just made, it shouldn't alter the original array at all.

If you know for sure that anything I've written is incorrect, or if you know other information pertinent to this issue, PLEASE share it here. This issue is far too important to leave to "It should just work" status. :-)

French answered 22/8, 2020 at 4:43 Comment(2)
There are always trade offs. I don't know, but it could be thread safe only if you don't optimize the binary above certain level or it's not under heavy load. I know swift string is reasonably fast, so I bet (sorry) it is not thread safe on modify.Ovolo
@Ovolo your assumption about optimisation level appears to be true, see my answer https://mcmap.net/q/1839161/-is-swift-39-s-copy-on-write-thread-safeSherilyn
S
0

TLDR: NO, it's not thread-safe.


Basically mentioned forum has a response from "eskimo" user, who is usually quite helpful and knowledgeable about Apple internals. Eskimo claims that there is no locks and arrays/dicts/etc. are NOT thread-safe. Another user called "xcoder112" claims that's not true and provides code which compiles and runs indefinitely while juggling array between threads and never fails or produce warning/errors. Additionally, user "sshirokov" claims that during their testing they found that arrays are NOT thread-safe.

I know, this answer might not be very satisfactory, but I just compiled the code from "xcoder11" with release optimisations:

swiftc -sanitize=thread -O test.swift && ./test

and immediately got a lot of thread sanitiser warnings like this:

➜  Downloads swiftc -sanitize=thread -O test.swift && ./test
test(30041,0x1f7a34c00) malloc: nano zone abandoned due to inability to reserve vm space.
==================
WARNING: ThreadSanitizer: data race (pid=30041)
  Write of size 8 at 0x000106c01490 by thread T1:
    #0 closure #1 in  <null>:43257476 (test:arm64+0x100002e58)
    #1 closure #1 in  <null>:43257476 (test:arm64+0x100002cf0)
    #2 __NSThread__start__ <null>:43257476 (Foundation:arm64e+0x54f7c)

  Previous read of size 8 at 0x000106c01490 by main thread (mutexes: write M0):
    #0 SomeClass.modifyNumbers() <null>:43257476 (test:arm64+0x10000330c)
    #1 main <null>:43257476 (test:arm64+0x100002c40)

  Location is heap block of size 120 at 0x000106c01480 allocated by main thread:
    #0 malloc <null>:43257476 (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x5609c)
    #1 swift_slowAlloc <null>:43257476 (libswiftCore.dylib:arm64e+0x3aa4c8)
    #2 specialized Array.replaceSubrange<A>(_:with:) <null>:43257476 (test:arm64+0x100003a90)
    #3 SomeClass.modifyNumbers() <null>:43257476 (test:arm64+0x10000332c)
    #4 main <null>:43257476 (test:arm64+0x100002c40)

  Mutex M0 (0x000106a01568) created at:
    #0 pthread_mutex_init <null>:43257476 (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x31470)
    #1 -[NSLock init] <null>:43257476 (Foundation:arm64e+0x41bc)
    #2 main <null>:43257476 (test:arm64+0x100002a9c)

  Thread T1 (tid=1556341, running) created by main thread at:
    #0 pthread_create <null>:43257476 (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x3062c)
    #1 -[NSThread startAndReturnError:] <null>:43257476 (Foundation:arm64e+0x7cf46c)
    #2 <null> <null> (0x00018f8720e0)

SUMMARY: ThreadSanitizer: data race (test:arm64+0x100002e58) in closure #1 in +0x158
==================

These warnings eventually slow down and almost stop completely (within one run), but that's probably due to branch predictor or some other low-level thing.

I'm not an expert on swift stdlib, copy-on-write or thread sanitizer (which might operate differently under release conditions and maybe even catch false positives), but this behaviour screams to me that I must use explicit locks or copies wherever I need to access/mutate arrays from different threads.

Sherilyn answered 19/8, 2024 at 9:13 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.