Reputation: 634
This is gonna be very basic question. I am having confusion regarding How structs are thread safe and classes are not.
Here is what I think it means to be thread safe and unsafe.
Thread Unsafe - If any object is allowed to modify by more than one thread at the same time.
Thread Safe - If any object is not allowed to modify by more than one thread at the same time.
Here is code for struct that I tried:
var basket = Basket()
func threadSafetyForStruct1() {
let queue = DispatchQueue(label: "threadSafety1", attributes: [.concurrent])
queue.async {
for age in 1...30 {
basket.amount = age
}
}
}
func threadSafetyForStruct2() {
let queue = DispatchQueue(label: "threadSafety2", attributes: [.concurrent])
queue.async {
for age in 31...60 {
basket.amount = age
}
}
}
threadSafetyForStruct1()
threadSafetyForStruct2()
Here is code for class I tried:
let animal = Animal()
func threadSafetyForClass1() {
let queue = DispatchQueue(label: "threadSafety1", attributes: [.concurrent])
queue.async {
for age in 1...30 {
animal.age = age
animal.name = "name\(age)"
}
}
}
func threadSafetyForClass2() {
let queue = DispatchQueue(label: "threadSafety2", attributes: [.concurrent])
queue.async {
for age in 31...60 {
animal.age = age
animal.name = "name\(age)"
}
}
}
threadSafetyForClass1()
threadSafetyForClass2()
Both code runs successfully. No runtime error nor compile time. Both have unexpected value at the end which is expected as I am running concurrent queue.
What I am missing here?
Upvotes: 1
Views: 3309
Reputation: 741
Thread-safety is a broad topic. However, the fact that you get unexpected values at the end of the program execution, is the indicator that the code is not thread-safe.
With that out of the way, I believe you wanted some sort of different outcome when using struct
and class
. Perhaps something like a crash? In that case, why doesn't it crash?
Int
and String
properties in the object which are value-types. I did some testing and found out that assignments to value-types in Swift tend to mostly be thread-safe. So your program doesn't crash.If your objects looked like this:
class Name{
var first: String = ""
var last: String = ""
init(){}
}
struct Animal{
var age: Int
var name: Name
init(){
age = 0
name = Name()
}
}
and you tried reading, and assigning new instances of Name
from two threads at once - now that has the potential to actually crash your program.
You will get an error like this at the point of assigning the new value:
malloc: Double free of object
The same will not happen if you were using a struct
implementation of Name. So does this make struct
thread-safe than class
? More thread-safe - yes. But your program's outcome is still unpredictable so it doesn't solve that issue.
In your code, you are doing 30 reads and assignments in each thread. There are no arbitrary complexity bits of code that may execute in-between those 30 operations.
If you were to introduce an artificial random delay (with something like usleep
) before each operation (and you were to specifically use class
instances) then you might encounter program crashes.
I made a small Git Repo of a version of your program that does manage to crash.
https://github.com/Thisura98/ConcurrentTest
Upvotes: 1
Reputation: 5073
You are observing inconsistent results because of context switching while your for-loops are executing. One loop runs for a while, then the other loops runs for a while, and they ping pong back and forth until the loops have terminated.
There is no guarantee that each loop will run start to finish without interruption from the other queue.
Just because something is a struct does not mean it is thread-safe. You have to protect the struct in areas that access the struct in from different queues. In this case, you need to protect the struct in your for-loops by ensuring that your loop starts and completes execution before the other queue has a chance to jump in.
One way to do that is by using NSLock
, but there are many ways to synchronize access to blocks of code.
var basket = Basket()
let basketLock = NSLock()
func threadSafetyForStruct1() {
let queue = DispatchQueue(label: "threadSafety1", attributes: [.concurrent])
queue.async {
basketLock.lock()
for age in 1...30 {
basket.amount = age
}
basketLock.unlock()
}
}
func threadSafetyForStruct2() {
let queue = DispatchQueue(label: "threadSafety2", attributes: [.concurrent])
queue.async {
basketLock.lock()
for age in 31...60 {
basket.amount = age
}
basketLock.unlock()
}
}
threadSafetyForStruct1()
threadSafetyForStruct2()
When the first queue obtains the lock, the other queue will be blocked until the first queue has released the lock. Only then can the second queue obtain the lock and do its thing.
Upvotes: 0
Reputation: 56477
First of all, structs are value types, meaning they are typically passed by value (copies are created). But not always. When you use them in closure (the function inside your queue.async
call) it is caught by reference. And so in that situation there's no real difference between a struct and a class.
Now, just because something is a struct doesn't make it automatically thread safe. Thread safety is a wide topic. One general definition that works is: a function (or collection of functions) is thread safe if whenever you execute them in parallel, the result is as if they were executed sequentially (in some arbitrary order). And so the usage of threads is only for optimization purposes, it doesn't change the behaviour in any way.
When you pass things by value, then copies are created. And so whatever you do on a copy, it will not affect the outside. In that sense things become thread safe, because resources are no longer shared. But when things are passed by reference, then you share the resource, and so thread safety issue occures.
Concluding: thread safety does not apply to the data itself (struct or class), but rather to the way you access the shared data.
Both have unexpected value at the end which is unexpected as I am running concurrent queue.
This is exactly as expected. You indeed have a queue, but since the dispatcher is concurrent, then there are multiple threads consuming that queue. And those threads run your internal functions in parallel. Without locks (or other synchronization primitives) your code is not thread safe, since multiple threads will access the data at the same time. The result is interleaved in best case scenario. In worst case it can crash completely (unless Swift guarantees assignments to be thread safe, which I'm not sure of).
Upvotes: 0