Home > database >  Are swift actors concurrent, but not parallel?
Are swift actors concurrent, but not parallel?

Time:07-04

I'm trying to figure out, what actor brings to use. It's not clear for me - are they truly parallel or just concurrent.

I did little test to check myself:

actor SomeActor {
    func check() {
        print("before sleep")
        sleep(5)
        print("after sleep")
    }
}

let act1 = SomeActor()
let act2 = SomeActor()
Task {
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            await act1.check()
        }

        group.addTask {
            await act2.check()
        }
    }
}

Output is:

before sleep // Waiting 5 sec
after sleep
before sleep // Starting after 5 sec
after sleep

I block current executor thread so it doesn't yield it. But second task doesn't start in parallel.

So does it mean that each instance of the same actor type share executor instance?

If so then multiple instance of same actor don't support truly parallelism, but concurrent execution?

UPD:

I have a little bit new observations. So example above use inherited isolation when we run group.add(). So it can't pass second actor call immediately.

My second observation - when we run tasks this way:

        let act1 = SomeActor()
        let act2 = SomeActor()
        
        Task.detached {
            await act1.check()
        }
        Task { @MainActor in
            await act2.check()
        }

Output is:

before sleep
before sleep
after sleep
after sleep

So it's truly parallel

But when I use detached task, output is serial the same:

        let act1 = SomeActor()
        let act2 = SomeActor()
        
        Task.detach {
            await act1.check()
        }
        Task.detached { @MainActor in
            await act2.check()
        }

Output is:

before sleep
after sleep
before sleep
after sleep

CodePudding user response:

It might help you if you add in some extra debug like so:

actor SomeActor {
    var name: String
    init(name: String) {
        self.name = name
    }

    func check() {
        print("Actor: \(name) check(), sleeping now, blocking thread \(Thread.current)")
        sleep(1)
        print("Actor: \(name), sleep done, unblocking thread \(Thread.current)")
    }
}

let act1 = SomeActor(name: "A")
let act2 = SomeActor(name: "B")
Task() {
    print("in task, on thread: \(Thread.current)")
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            print("Group task: 1 (on thread: \(Thread.current)")
            await act1.check()
        }

        group.addTask {
            print("Group task: 2 (on thread: \(Thread.current)")
            await act2.check()
        }
    }
}

The Task is like dispatching onto a global queue, the priority of which is taken from Task.init's priority: argument

If you block the Thread that the task is using, with sleep (don't do that) that's not yielding the thread. If you use the async sleep, the try? await Task.sleep then you see the cooperative behaviour:

actor SomeActor {
    var name: String
    init(name: String) {
        self.name = name
    }

    func check() async {
        print("Actor: \(name) check(), sleeping now, yielding thread \(Thread.current)")
        try? await Task.sleep(nanoseconds: NSEC_PER_SEC)
        print("Actor: \(name), Task.sleep done (was not cancelled), back now on thread \(Thread.current)")
    }
}

let act1 = SomeActor(name: "A")
let act2 = SomeActor(name: "B")
Task() {
    print("in task, on thread: \(Thread.current)")
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            print("Group task: 1 (on thread: \(Thread.current)")
            await act1.check()
        }

        group.addTask {
            print("Group task: 2 (on thread: \(Thread.current)")
            await act2.check()
        }
    }
}

Gives output like:

in task, on thread: <NSThread: ...>{number = 4, name = (null)}
Group task: 1 (on thread: <NSThread:...>{number = 4, name = (null)}
Actor: A check(), sleeping now, yielding thread <NSThread: ...>{number = 4, name = (null)}
Group task: 2 (on thread: <NSThread: ...>{number = 4, name = (null)}
Actor: B check(), sleeping now, yielding thread <NSThread: ...>{number = 4, name = (null)}
Actor: A, Task.sleep done (was not cancelled), back now on thread <NSThread: ...>{number = 7, name = (null)}
Actor: B, Task.sleep done (was not cancelled), back now on thread <NSThread: ...>{number = 7, name = (null)}

CodePudding user response:

Short answer: yes and no, but mostly no. Actors do serialize the execution of tasks on them, however, the async/await guarantee that the threads occupied by those tasks are free to execute other tasks.

Longer answer: it depends on what you understand by parallelism. If we're talking about code executing on the actor, then definitively actors are not concurrent.

Actors need to maintain internal state consistency and avoid data races, so at any point in time, only one task can execute on a given actor.

Now, this doesn't mean actors block threads, as any await call puts the task in a "suspended" state, which frees the thread to be occupied by another task. However, this behavior is not related to actors, it's just another part of Swift's structured concurrency.

  • Related