Race condition behaviors

In case of Int you may also observe 0, because non-atomic writes may be not immediately visible to other threads.

In case of multi-word values (SIMD, Codable) you may observe Frankenstein values where some words belong to the old value and some to the new one.

In case of references you may see wrong instances being retained or released. This may lead to objects being deallocated while still being used or never deallocated.

@YuAo, I wonder why are you asking. You are not writing any production code that would rely on behavior of the data races, do you? Data races must be avoided, not supported.

Just curious about how multi-thread, multi-core and swift works.

I think the Codable case is complicated. Since the conforming type may be as simple as an Int or a multi-word struct or even a class that need to be reference counted. This may bring up the " existentials"?

Codable when used as a type, is always an existential (any Codable), and occupies 5 words - 3 for the value buffer, 1 for the meta-type, and one for the protocol witness table. But that is irrelevant, you must avoid data races regardless of the type.

How about:

It's ok that the update is not instantly visible by the main thread. The display link may get the value in the next run.

I don't know where you got that, but that's not true. Even the simplest types like Byte or Int would cause data races (== app crash) if you read/write them from different threads. Xcode -> Edit Scheme -> Diagnostics -> Thread Sanitizer is your friend.

I does not get that from anywhere, that is the question.

I think it's better to bring this discussion to an end.

I found a two years old similar post and "jonathanpenn" summarized very well, thanks jonathanpenn.

This is deep down a very complex topic. I think need to do more research.

if you have a recent toolchain, the compiler will tell you the answer!

class Counter 
{
    var value:Int = 0
}
@main 
enum Main 
{
    static 
    func main() async 
    {
        let counter:Counter = .init()

        let thread:(@Sendable () async -> (), @Sendable () async -> ()) = 
        (
            {
                while true 
                { 
                    counter.value = 1 
                }
            }, 
            {
                while true 
                { 
                    counter.value = 2 
                }
            }
        )
        
        async let first:Void    = thread.0()
        async let second:Void   = thread.1()

        let v:Int = counter.value 
        print(v)
        
        await first
        await second
    }
}
$ swiftc example.swift -parse-as-library
example.swift:19:21: warning: cannot use let 'counter' with a non-sendable type 'Counter' from concurrently-executed code
                    counter.value = 1 
                    ^
example.swift:1:7: note: class 'Counter' does not conform to the 'Sendable' protocol
class Counter 
      ^
example.swift:25:21: warning: cannot use let 'counter' with a non-sendable type 'Counter' from concurrently-executed code
                    counter.value = 2 
                    ^
example.swift:1:7: note: class 'Counter' does not conform to the 'Sendable' protocol
class Counter 

Emm... My question is: what is actually happening when there's a race condition. The complier just told us there may be a race condition.

On a formal level, such a data race has undefined behavior, and Swift is not constrained to do anything sensible.

On an implementation level, Swift will usually keep different values in non-overlapping, well-aligned memory, and it will usually perform loads and stores of small, trivial types using single load and store instructions, which are usually architecturally guaranteed to not introduce spurious tearing on well-aligned memory. However, the implementation isn't required to do any of these things, because data races have undefined behavior. And this is not merely theoretical for most types:

  • Loads and stores of non-trivial types can corrupt the heap under a data race. For example, two concurrent stores of a class reference can race to release the same object instead of being arbitrarily ordered.
  • Loads and stores of trivial types will not directly crash under data races, but they may exhibit tearing or other "unnatural" behavior. For example, there are situations in which Swift can end up copying an Int with memcpy, such as if it's part of a larger aggregate. There is no guarantee that Swift will use "natural" loads and stores from the underlying architecture.
  • Load and stores from different stored properties of a class will not interfere with each other, at least for all current classes. However, this guarantee may be weakened in the future, and it does not extend to structs. For example, Swift might choose to "pack" two different Bool struct properties into the same byte of storage, and it does not have to use atomic sequences to update them. (Swift does not currently perform this optimization, but it's permitted because of the rules about data races on structs. It would not be permitted for class properties.)
17 Likes

There are also some highly unusual machine-dependent possible issues, e.g. on Alpha (may it rest in peace, because I certainly don't want to deal with it!), "…writes to a shared byte, word, or longword may corrupt other data present in the same quadword as the shared data." (from Migrating an Application from OpenVMS VAX to OpenVMS Alpha)

4 Likes

Is it actually permitted? That would mean MemoryLayout.offset(of:) could return nil or not based on the whims of the optimiser, or the target platform.

Packing trivial struct fields in to unaligned storage is probably fine (each field would still have a byte offset), but packing multiple fields as a single byte would probably require some kind of opt-in attribute, wouldn’t it?

It'll probably return the same byte offset for several bool fields packed in the same byte.

No, that cannot work. You'd at least know the offset (in bits) for it to be any useful, which is not something offset(of:) can provide.

It seems to be mentioned in a separate thread that they indeed are not addressable.

I tried it now on mac and it returned different offsets for different bool fields put close together in the same struct. I guess it's not wise to depend on this behaviour given what @John_McCall said (as it might change in the future?). When it will actually change and start returning "nil" for offset in that same swift version I'd expect to see some "bitOffset" API to grab field offset in bits.

BTW, what's the proper way to determine field size? There is no size(of: PartialKeyPath) similar to offset. I can perhaps use the "next" field's offset, and calculate the difference between the two, but that will potentially include some alignment bytes. Tried using Mirror + children + MemoryLayout.size(ofValue: child.value) but that always return 32 (size of Any).

I'm not sure what you're looking for, but the size of an (addressable) field would be determined by its type. So you could just use MemoryLayout<Field>.size. The stride (between the same field on consecutive containers) would be the stride of the container type.

PS

We can also spin up a new thread if you'd like, this is getting further from the topic of race condition.

I am taking about something like this:

public static func size(of key: PartialKeyPath<T>) -> Int?

defined on enum MemoryLayout, that returns correct result, e.g. it returns 1 if the bool field is stored normally and nil if several bool fields are packed into a single byte.

That’s correct, it can, at least for Swift-defined types. If this isn’t documented, it should be.

4 Likes

extension MemoryLayout {
    static func size<Value>(ofFieldAt path: KeyPath<T, Value>) -> Int? {
        MemoryLayout.offset(of: path).map { _ in MemoryLayout<Value>.size }
    }
}

If we're continuing, let's make a new thread.

2 Likes

I must say this is a genius hack! It answers my question so no need for an extra thread.