Apple's description of reference and value types with multiple threads
Asked Answered
B

5

7

I am reading from Apple's documentation. I thought I knew when to choose a value type and when to choose a reference type, but I am back to Swif101. The documentation says:

  • Value Types: The data will be used in code across multiple threads.
  • Reference Types: You want to create shared, mutable state

Aren't reference types also shared across multiple threads? What's the difference in these two lines?

Bradway answered 11/8, 2016 at 18:9 Comment(0)
F
10

As others have pointed out, reference types always pass a pointer to the object, which is ideal where you want a "shared, mutable state" (as that document you referenced said). Clearly, though, if you're mutating/accessing a reference type across multiple threads, make sure to synchronize your access to it (via a dedicated serial queue, the reader-writer pattern, locks, etc.).

Value types are a little more complicated, though. Yes, as the others have pointed out, if you pass a value type as a parameter to a method that then does something on another thread, you're essentially working with a copy of that value type (Josh's note regarding the copy-on-write, notwithstanding). This ensures the integrity of that object passed to the method. That's fine (and has been sufficiently covered by the other answers here).

But it gets more complicated when you are dealing with closures. Consider, for example, the following:

struct Person {
    var firstName: String
    var lastName: String
}

var person = Person(firstName: "Rob", lastName: "Ryan")

DispatchQueue.global().async {
    Thread.sleep(forTimeInterval: 1)
    print("1: \(person)")
}

person.firstName = "Rachel"
Thread.sleep(forTimeInterval: 2)
person.lastName = "Moore"
print("2: \(person)")

Obviously, you wouldn't generally sleep, but I'm doing this to illustrate the point: Namely, even though we're dealing with a value type and multiple threads, the person you reference in the closure is the same instance as you're dealing with on the main thread (or whatever thread this was running on), not a copy of it. If you're dealing with a mutable object, that's not thread-safe.

I've contrived this example to illustrate this point, where the print statement inside the closure above will report "Rachel Ryan", effectively showing the state of the Person value type in an inconsistent state.

With closures using value types, if you want to enjoy value semantics, you have to change that async call to use a separate variable:

let separatePerson = person
queue.async {
    Thread.sleep(forTimeInterval: 1)
    print("1: \(separatePerson)")
}

Or, even easier, use a "capture list", which indicates what value type variables should be captured by the closure:

queue.async { [person] in
    Thread.sleep(forTimeInterval: 1)
    print("1: \(person)")
}

With either of these examples, you're now enjoying value semantics, copying the object, and the print statement will correctly report "Rob Ryan" even though the original person object is being mutated on another thread.

So, if you are dealing with value types and closures, value types can be shared across threads unless you explicitly use capture list (or something equivalent) in order to enjoy value semantics (i.e. copying the object as needed).

Fisken answered 27/12, 2016 at 23:50 Comment(3)
when I first read the answers here, I thought under NO circumstance value type would have a multi-threading issue. But that's only correct if you capture/copy. If you are manipulating the exact same value then you have problems...Bradway
Value types are a little more complicated, though. Yes, as the others have pointed out, if you pass a value type as a parameter to a method that then does something on another thread <-- Aren't you writing is slightly confusing? The struct can't be mutated when passed to another function of the same thread. Say I passed a struct into a func, the parameter inside the func is immutable...Bradway
Yes, if you pass a value type to method, that method receives a copy of it, so if you mutate it, you're mutating a copy and the original is safely untouched. But as I say in the next paragraph, the "separate thread gets copy of value type" discussion is more complex, because this copying behavior doesn't take place when using closures. And the original question was not about passing value types as parameters, but rather more generally about whether other threads always get copies of value types. Bottom line, value types used in closures are not copied (without capture lists, at least).Fisken
E
4

Yes, references are shared if multiple threads use them; that's exactly the problem. All threads refer to the same actual data in memory. They then require synchronization mechanisms to ensure that the separate threads' reads and writes don't conflict. Those mechanisms have costs in code complexity and in performance.

Instances of value types are not shared: every thread gets its own copy.* That means that every thread can read and write to its instance without having to worry about what other threads are doing.


*With the standard copy-on-write exception for Swift stdlib types: the actual copy is only performed if the data are mutated.

Elaterite answered 11/8, 2016 at 18:16 Comment(0)
H
3

That's confusingly worded.

Value Types: The data will be used in code across multiple threads.

By this, I believe that they mean it's useful when you want many threads to read from your data. This is because you know that whenever you give a new thread a copy of your data, you don't subject any other copies to the risk of unexpected mutation from other threads.

Using value types in such a context, where you don't need shared mutable state, allows you to avoid many classes of bugs (race conditions, dead lock, live lock, etc.) that come from dealing with reference types.

Reference Types: You want to create shared, mutable state

Only reference types can be shared between threads, by having two threads each retain their own reference to a shared instance.

Hanghangar answered 11/8, 2016 at 18:16 Comment(0)
F
0

Apple suggests using a value type when the data will be used across multiple threads to avoid using a shared instance, rather than promoting it.

When you provide your threads with an object of value type, each thread would get its own copy, which in turn lets you not worry about synchronization.

Generally, you should prefer making your value type immutable (although Swift provides a mechanism for you to code mutating functions). When you do that, you have no synchronization problems, even with reference types.

Feckless answered 11/8, 2016 at 18:16 Comment(0)
P
-1

Value Types: The data will be used in code across multiple threads.

The data will not change across the threads once assigned. No worries to other threads since each thread would have its own copy.

Reference Types: You want to create shared, mutable state

The data may change by any of the threads which would affect the other threads as well since its of type reference(ie. pointing to same data in memory).

Procession answered 11/8, 2016 at 18:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.