I often use this pattern to save the newest value to file.
struct Foo {
var value: Int {
didSet {
Task {
// call an async func to save the value
}
}
}
}
Complete Code Example (I added debugging code to print value in a few places)
import Foundation
import AsyncAlgorithms
actor FileService {
static let shared = FileService()
let url = URL.documentsDirectory.appending(path: "myfile")
let (datum, cont) = AsyncStream.makeStream(of: Data.self)
init() {
Task {
for await data in datum {
let value = try! JSONDecoder().decode(Int.self, from: data)
print("value in actor [consumer part]: \(value)")
try data.write(to: url, options: .atomic)
}
}
}
func save(data: Data) {
let value = try! JSONDecoder().decode(Int.self, from: data)
print("value in actor [provider part]: \(value)")
cont.yield(data)
}
}
struct Foo {
var value: Int = 1 {
didSet {
Task { [value = self.value] in
print("value in client side task: \(value)")
let data = try JSONEncoder().encode(value)
await FileService.shared.save(data: data)
}
}
}
}
func test() {
var foo = Foo()
foo.value = 2
//sleep(4)
foo.value = 3
sleep(10)
}
test()
I didn't think a lot about the code. I thought it should just work (it worked fine in practice indeed). However, as I'm refactoring the code, I test it and find unexpected behavior - the order isn't guaranteed.
You can run the above code to see the output yourself. It generates different output almost in every run.
Note: there is a key setup in the code - there should be no delay between foo.value = 2 line and foo.value = 3 line in test().
Example output 1:
value in client side task: 3
value in client side task: 2
value in actor [provider part]: 2
value in actor [provider part]: 3
value in actor [consumer part]: 2
value in actor [consumer part]: 3
Example output 2:
value in client side task: 2
value in client side task: 3
value in actor [provider part]: 3
value in actor [provider part]: 2
value in actor [consumer part]: 3
value in actor [consumer part]: 2
Example output 3:
value in client side task: 3
value in client side task: 2
value in actor [provider part]: 3
value in actor [consumer part]: 3
value in actor [provider part]: 2
value in actor [consumer part]: 2
My observations:
-
First, the order of value in client side task is not fixed (see log above. It was "3, 2" or "2, 3"). This surprised me a bit. The code that prints the log runs in the same actor context, so they are executed in serial. I had thought they were scheduled using a FIFO strategy in this case and hence would run in order, but the output shows it's not true.
-
Second, the order of "value on client side" and "value in actor [provider part]" don't always match. Take the output 1 as an example:
client side task: 3, 2
actor [provider part]: 2, 3
So, even if I was able to maintain the correct order in client side task, the await <someActorMehtod> call would cause the someActorMethod see the values in different order? While I might be able to understand the behavior (I personally would like to understand async func as coroutine, and the way how coroutines are scheduled determines their execution order), I find this is anti-intuitative (I don't mean it should have a different behavior, I just think it's tricky to use correctly).
Given the above observations, it seems the ordering assumption in the pattern I described at the beginning is completely wrong? When value changes not very fast, the potential issue is hided and code works as expected. However, if value changes fast enough, the behavior will become weird and hard to debug.
If so, I wonder what's the correct way to implement it? My requirement is very simple: whenever a value changes, I'd like to call an actor's method and that method should run in order.
Thank for any suggestions.