swiftasync-awaitactor

How wait some job in global actor


There is a data storage for reading and writing data to a dictionary. I'm using a global actor that guarantees serial operation execution.

Code example:

@globalActor
struct DataStorageActor {
    actor ActorType { }
    static let shared: ActorType = ActorType()
}

@DataStorageActor
class CustomDataStorage {
    private var storage: [String: String] = [:]
    
    func write(key: String, value: String) async throws {
        print("\(Date()): Write started")
        try await Task.sleep(nanoseconds: 5_000_000_000)
        storage[key] = value
        print("\(Date()): Write completed for key: \(key)")
    }
    
    func read(key: String) async throws -> String? {
        print("\(Date()): Read started")
        let value = storage[key]
        print("\(Date()): Read completed for key: \(key)")
        return value
    }
}

And a unit test was written:

    @Test(arguments: ["Very long string"])
    func testSerialExecution(input: String) async throws {
        let storage = CustomDataStorage()

        let task1 = Task {
            try await storage.write(key: "test", value: input)
        }

        let task2 = Task {
            try await Task.sleep(nanoseconds: 2_000_000_000)
            
            let value = try await storage.read(key: "test")
            print("\(Date()): Received value: \(value ?? "nil")")
            
            return value
        }
        
        print("wait task 1")
        _ = try await task1.value
        
        print("wait task 2")
        let value: String? = try await task2.value
        
        print("check result")
        #expect(value == input)
    }
  1. In a separate thread, I launch a long write operation (5 seconds)
  2. In a separate thread, I launch a read operation with a 2-second delay
  3. I expect the read operation to wait for the write operation to complete

But the read operation runs in parallel, logs:

wait task 1
2025-04-07 12:43:03 +0000: Write started
2025-04-07 12:43:05 +0000: Read started
2025-04-07 12:43:05 +0000: Read completed for key: test
2025-04-07 12:43:05 +0000: Received value: nil
2025-04-07 12:43:08 +0000: Write completed for key: test
wait task 2
check result

Why does it work like that?


Solution

  • This is because during try await Task.sleep(nanoseconds: 5_000_000_000), or rather, every time an await happens, the actor becomes free to do other tasks. In other words, actors are reentrant.

    In your real code, if you are not awaiting for anything in write, the state of the storage will be protected by the actor.

    If you do await something in write, then other code can run on the actor during that await. For solutions to this, see How to prevent actor reentrancy resulting in duplicative requests?