Hey, I just started learning about Swift Concurrency and was wondering if it would be possible to write code like this:
class GameScene: SKScene {
var imagesToUpload = ["cat1", "mouse1", "dog1"]
var lastUploaded = ""
func uploadAllImages() async {
await withTaskGroup(of: Void.self) { group in
for name in imagesToUpload {
group.addTask {
await self.uploadImage(of: name)
self.lastUploaded = name
}
}
}
}
func uploadImage(of: String) async {
try! await Task.sleep(for: .seconds(1))
}
}
Over here I have a class which inherits from SKScene, which at some point inherits from UIResponder which is declared with @MainActor. Thus, my GameScene class also inherits the @MainActor attribute. The issue with the code above is that I'll have to mark the .addTask { } closure with @MainActor. I was wondering if it would be possible to somehow make the stored property nonisolated (or somehow remove the inherited @MainActor attribute). I want to do this because it's my understanding that if multiple threads do write to the lastUploaded property that won't be a problem in this situation.
Also, it's just bothering me that I can write the code above using completion handlers without issue:
class GameScene: SKScene {
var imagesToUpload = ["cat1", "mouse1", "dog1"]
var lastUploaded = ""
func uploadAllImages() {
for name in imagesToUpload {
uploadImage(of: name) {
self.lastUploaded = name
}
}
}
func uploadImage(of: String, completion: @escaping () -> Void) {
// Uploads photo
}
}
So if it's possible to make the code in the 1st snippet work please let me know.
It is absolutely a problem. Multiple threads cannot safely write to a single location in memory without some form of synchronization or use of atomics.
Your example leaves out the part that actually invokes the completion handler, so it’s impossible to say whether there’s an issue.
Hey, thanks for the reply. I read a little about data corruption and wanted to know if this is a situation that would cause data corruption or a crash:
ThreadA writes to a memory location
Then ThreadB writes to same memory location
ThreadA writes the first byte
Then ThreadB writes the first byte
ThreadB writes the second byte
then ThreadA writes the second byte
ThreadA writes the third and fourth byte
then ThreadB writes the third and fourth byte
With this order, the data has values [fromB, fromA, fromB, fromB] which may lead to completely incorrect results.
However, I am assuming that it's possible that when a write operation starts, the amount of time taken for each write operation is not consistent. Writing the first two bytes may be significantly quicker than writing the other two, and in that time another thread could come which finishes writing all 4 bytes in the time between writing your 2nd and 3rd byte.
Am I right in this assumption and is this an example of how a "problem" might occur with multiple threads writing to the same memory location?
There's a lot to cover when it comes to threads, reads and writes. All the scenarios you wrote above are subject to race conditions (and memory corruption) in most situations unless you use some kind of synchronization mechanism (mutexes, atomics, dispatch queues, actors, etc.).
Writing a byte is often a read-modify-write operation where you read a word (size of a word depends on the architecture), change one byte in it, and then write back the word.
Changing a string (or another Swift container, or an any class for that matter) is also a read-modify-write operation. With bad luck one thread will read the address while the other is setting it to nil or another value causing the deallocation of what the first thread just read. The first thread would end up with a dangling reference before it even has a chance to retain it.
In all these cases you need to ensure only one thread has access during a read-modify-write operation. You can't rely on timing at all: even if you could ensure proper timing, the cache on each core might have a different view of what's in memory at a given time. It takes some time for changes to propagate and get in sync between two cores, and the only way to be sure this propagation has occurred is to use memory barrier instructions which are the foundation of all synchronization primitives.