I'm currently working with Swift's concurrency model and have encountered challenges when performing sequential operations on an actor while maintaining its isolation. Below is a simplified version of my code:
actor CountDatabase: Sendable {
private var value: Int = 0
func write(_ newValue: Int) {
value = newValue
}
func read() -> Int {
value
}
}
func isolate(_ actor: isolated CountDatabase, _ closure: @Sendable @escaping (CountDatabase) -> Void) {
print("isolate operates on")
closure(actor)
}
let database = CountDatabase()
Task {
await isolate(database) { database in
database.write(10)
let ten = database.read()
print("Actor: ten is \(ten)")
database.write(ten + 1)
let eleven = database.read()
print("Actor: eleven is \(eleven)")
}
}
The current approach feels cumbersome and not in line with Swift's emphasis on clarity and conciseness.
I would expect to do that something like
Task { [isolate database] in
database.write(10)
let ten = database.read()
print("Actor: ten is \(ten)")
database.write(ten + 1)
let eleven = database.read()
print("Actor: eleven is \(eleven)")
}
Is there a more straightforward way to perform multiple sequential operations on an actor?
if not, what are the underlying reasons for the absence of such a feature in Swift's concurrency model?
Thank you for the quick response @ktoso! I came across that proposal and initially thought it might have evolved into something else during the review process. I couldn’t find a final version, so it seems it’s still in the works. Looking forward to its progress!
@OguzYuuksel, thank you for creating this interesting and high-value topic.
I couldn't stop myself tinkering with it.
My Tinkering
//
// Test.swift
// Actors
//
// Created by ibex on 16/1/2025.
//
// [https://forums.swift.org/t/how-to-create-a-local-actor-isolated-task/77225]
// [https://forums.swift.org/t/how-to-create-a-local-actor-isolated-task/77225/4]
@main
enum Test {
static func main () async throws {
let x: Self?
print (type (of: x))
let N = 64
let database = CountDatabase()
let signal = Signal()
print ("spawn some work...")
let values: [Int] = [16, 32, 64, 67]
for i in 0..<values.count {
spawn (key: i, value: values [i], load: N, database: database, signal: signal)
}
print ("finished spawning...")
print ("wait for all work to finish...")
while true {
let u: Bool = await signal.performOperation { sig in
let u = sig.read()
let v = u == values.count
return v
}
if u {
break
}
else {
try await Task.sleep(until: .now + .seconds (1))
}
}
print ("finished!")
}
}
extension Test {
static func spawn (key: Int, value: Int, load N: Int, database: CountDatabase, signal: Signal) {
Task.detached {
for i in 0..<N {
await database.performOperation { db in
db.write (value)
let u = db.read ()
print (key, i, "\(value) is \(u)")
assert (u == value)
db.write (u + 1)
let v = db.read ()
print (key, i, "\(value + 1) is \(v)")
assert (v == value + 1)
}
}
// signal finished
await signal.performOperation { sig in
let u = sig.read()
sig.write (u + 1)
}
}
}
}
actor CountDatabase {
private var value: Int = 0
func write(_ newValue: Int) {
value = newValue
}
func read() -> Int {
value
}
}
extension CountDatabase {
func performOperation(_ op: @Sendable (isolated CountDatabase) -> Void) {
op(self)
}
func performOperation<T, E>(_ op: @Sendable (isolated CountDatabase) throws(E) -> sending T) throws(E) -> sending T {
try op(self)
}
}
actor Signal {
private var value: Int = 0
func write(_ newValue: Int) {
value = newValue
}
func read() -> Int {
value
}
}
extension Signal {
func performOperation(_ op: @Sendable (isolated Signal) -> Void) {
op(self)
}
func performOperation<T, E>(_ op: @Sendable (isolated Signal) throws(E) -> sending T) throws(E) -> sending T {
try op(self)
}
}