티스토리 뷰

🍏/Swift

[Swift] actor

eungding 2023. 4. 10. 00:01
반응형

[ 이 글에서 다루는 내용 ]

- actor 의 등장배경

- actor

- nonisolated

- global actor

- main actor 

 

 

 

[1] actor 가 나오기 전에는? 

예전에는  data race가 발생할 수 있다면  lock 이나 queue 를 추가 구현해줘야했다. 

 

대충 data race 가 발생할 수 있는 상황을 만들고, Thread Sanitizer 를 켜고 돌려보자. 

class UserManager {
    
    static let shared = UserManager()
    
    private(set) var userName = ""
    
    private init() {}
    
    func updateUserName(to name: String) {
        print(Thread.current)
        userName = name
    }
}

struct MyView: View {
    
    var body: some View {
        VStack {
            Text("Tom")
                .onAppear {
                    DispatchQueue.global().async {
                        UserManager.shared.updateUserName(to: "Tom")
                    }
                }
            
            Text("Jane")
                .onAppear {
                    DispatchQueue.global().async {
                        UserManager.shared.updateUserName(to: "Jane")
                    }
                }
        }
    }
}

 

서로 다른 쓰레드가 동시에 값을 변경하려고 해서, data race 워닝이 뜬다. 

 

 

이때 lock을 사용하거나 

class UserManager {
    
    ... 
    private let lock = NSLock()
    
    func updateUserName(to name: String) {
        lock.lock()
        
        print(Thread.current)
        userName = name
        
        lock.unlock()
    }
}

 

queue 를 사용하거나

class UserManager {
    
    ... 
    private let queue = DispatchQueue(label: "com.MyApp.UserManager")

    func updateUserName(to name: String) {
        queue.sync {  // 예제 코드에선 async로 해줘도 상관없으나 sync를 사용하겠음. 
            print(Thread.current)
            self.userName = name
        }
    }
}

 

해서 대응을 해줬다. 

 

 

[2] actor

하지만 이제 actor를 사용하면 된다. 

actor는 thread safe 한 class 이다.   (no concurrent access!  actor isolation🏝️)  // 개념설명은 이 글 6번 참고 

그래서 actor를 쓰면 우리는 별도 대응해줘야할게 없다! 

✅ actor UserManager {
    
    static let shared = UserManager()
    private(set) var userName = ""
    
    private init() {}
    
    func updateUserName(to name: String) {
        print(Thread.current)
        self.userName = name
    }
}

struct MyView: View {
    
    var body: some View {
        VStack {
            Text("Tom")
                .onAppear {
                    ✅ Task {
                        await UserManager.shared.updateUserName(to: "Tom")
                    }
                }
            
            Text("Jane")
                .onAppear {
                    ✅ Task {
                        await UserManager.shared.updateUserName(to: "Jane")
                    }
                }
        }
    }
}

 

출력되는 thread 정보를 보면 main thread 에서 실행되지 않음을  알 수 있다. (=Task의 priority가 잘 지정되었음) 

그래도 확실한 것을 원한다면 priority 를 직접 지정해줘도 상관없다.  

Task(priority: .background) {
     await UserManager.shared.updateUserName(to: "Tom")
}

 

더 확실하게 테스트하기 위해 onAppear 말고 타이머를 도입해보자

struct MyView: View {
    
    let timer = Timer.publish(every: 0.1, on: .main, in: .common)
        .autoconnect()  // / 뷰가 create 될 때 자동으로 연결됨.
    
    var body: some View {
        VStack {
            Text("Tom")
                .onReceive(timer) { _ in
                    Task {
                        await UserManager.shared.updateUserName(to: "Tom")
                    }
                }
            
            Text("Jane")
                .onReceive(timer) { _ in
                    Task {
                        await UserManager.shared.updateUserName(to: "Jane")
                    }
                }
        }
    }
}

 

0.1초마다 계속 되는 access에도

data race 워닝이 안찍히는 것을 확실히 알 수 있다. 

 

 

 

[3] actor 는 FIFO 가 아니라 우선순위 기법 사용 

 

actor는 first-in, first-out 방식이 아니라 우선순위가 높은 작업을 먼저 실행한다고 한다. 

 

 

 

[4] nonisolated 

actor 내에서 isolation이 필요없는 경우가 있을 수 있다.

 

immutable 프로퍼티는 let을 보고 구분하는 것 같고

function은 구분하기 어려우니 (하드코딩 값을 리턴한다 해도?)  await을 컴파일러가 요구한다. 

actor UserManager {

    let userId = "..."

    func getUserId() -> String {
        return "..."
    }
}

struct MyView: View {
    var body: some View {
        VStack {
            SubView1()
            SubView2()
        }
        .task {
            print(UserManager.shared.userId)
            print(await UserManager.shared.getUserId())
        }
    }
}

 

이때 nonisolated 를 사용하면 된다.

(원한다면 let 앞에도 붙여줘도 상관없다) 

actor UserManager {

    nonisolated let userId = "..."
    
    nonisolated func getUserId() -> String {
        return "..."
    }
}

struct MyView: View {
    var body: some View {
        VStack {
            SubView1()
            SubView2()
        }
        .task {
            print(UserManager.shared.userId)
            ✅ print(UserManager.shared.getUserId())
        }
    }
}

 

 

[5] Global Actor

 

Global Actor 는 globally-unique actor 이다.

A type that represents a globally-unique actor that can be used to isolate various declarations anywhere in the program

 

즉 single actor type에서 사용할 수 있었던 isolation 개념을 글로벌로 확장한 것이다.

이를 통해 global state, function 또는 다양한 곳에 있는 state, function들이 actor isolation의 혜택을 얻을 수있다.

 

 

위의 예제를 살짝 변형해보자

ViewModel이 있고 오직 updateUserName 메소드만 isolation이 필요한 상황이라고 해보자.

이 상태로 돌려보면 data race 워닝이 뜬다. 

class MyViewModel: ObservableObject {
    
    private(set) var userName = ""

    func doSomething1() {
        //  ...
    }
    
    func doSomething2() {
        // ... 
    }
    
    func updateUserName(to name: String) {
        print(Thread.current)
        self.userName = name
    }
}

struct MyView: View {
    
    let timer = Timer.publish(every: 0.1, on: .main, in: .common)
        .autoconnect()
    
    @StateObject private var viewModel = MyViewModel()
    
    var body: some View {
        VStack {
            Text("Tom")
                .onReceive(timer) { _ in
                    DispatchQueue.global().async {
                        viewModel.updateUserName(to: "Tom")
                    }
                }
            
            Text("Jane")
                .onReceive(timer) { _ in
                    DispatchQueue.global().async {
                        viewModel.updateUserName(to: "Jane")
                    }
                }
        }
    }
}

 

updateUserName은 actor에 있지 않는 task인데, 어떻게 isolated 시킬까?

global actor를 통해 할 수 있다. 

 

global actor를 만들어보자.

GlobalActor 프로토콜은 이런 요구사항을 가지고 있다. (Required 참고) 

 

 

 

그래서 이렇게 만들어주었다. 

@globalActor
struct MyGlobalActor {
    actor MyActor {}
    static let shared = MyActor()
}

 

사실 MyGlobalActor 타입을 세개 중 뭐로 해야 가장 적절한 지 모르겠는데..

(참고로 대표적인 global actor의 예인 MainActor는 actor 타입임) 

@globalActor
struct MyGlobalActor {..}

@globalActor
actor MyGlobalActor {..}

@globalActor
final class MyGlobalActor {..}

struct로 해도 될 것 같아서 struct로 해줬다. 

 

 

이제 뷰모델의 updateUserName에 @MyGlobalActor attribute를 적용해서 actor isolation의 benefit을 쉽게 누릴 수 있다. 

@globalActor
struct MyGlobalActor {
    actor MyActor {}
    static let shared = MyActor()
}

class MyViewModel: ObservableObject {
    
    ... 
    
    @MyGlobalActor
    func updateUserName(to name: String) {
        print(Thread.current)
        self.userName = name
    }
}

struct MyView: View {
    ... 
    @StateObject private var viewModel = MyViewModel()
    
    var body: some View {
        VStack {
            Text("Tom")
                .onReceive(timer) { _ in
                    Task {
                        await viewModel.updateUserName(to: "Tom")
                    }
                }
            
            Text("Jane")
                .onReceive(timer) { _ in
                    Task {
                        await viewModel.updateUserName(to: "Jane")
                    }
                }
        }
    }
}

 

global actor니까 물론 다른 곳에서도 사용될 수 있다. 

대충 예제를 만들어보면 다음과 같다. 

class SomeClass {
    @MyGlobalActor var text1 = ""
    @MyGlobalActor var text2 = ""
    @MyGlobalActor var text3 = ""
    
    @MyGlobalActor
    func update() {
        text1 = "~~"
    }
}

struct SomeView: View {
    var body: some View {
        VStack {}
            .task {
                let some = SomeClass()
                let text1 = await some.text1
                
                await some.update()
            }
    }
}

 

그리고 위의 경우 처럼, @MyGlobalActor 가 전반적으로 쓰인다고 하면 

class 전체에 적용할 수도 있다. 

@MyGlobalActor
class SomeClass {
    var text1 = ""
    var text2 = ""
    var text3 = ""
    
    nonisolated init() {}
    
    func update() {
        text1 = "~~"
    }
}

struct SomeView: View {
    var body: some View {
        VStack {}
            .task {
                let some = SomeClass()
                let text1 = await some.text1
                
                await some.update()
            }
    }
}

 

참고로 

@MyGlobalActor 를 class 전체에 적용하면 

initializer에도 적용되어서 이런 식으로 생성을 해줘야하므로 initializer만 nonisolated 해줬다. 

let some = await SomeClass()

 

 

 

[6] Main Actor

MainActor  는 메인쓰레드에서 동작하는 global actor 이다. 

 

 

문서에 나온 것처럼 main dispatch queue 와 동일한 역할을 한다.

예를들어 백그라운드 작업을 하다가 UI 업데이트가 필요하면  MainActor.run 을 활용할 수 있다. 

class ViewModel: ObservableObject {
    
    @Published var image: UIImage? = nil
    
    func fetchImage() async {
        do {
            guard let url = URL(string: "https://picsum.photos/1000") else { return }
            let (data, _) = try await URLSession.shared.data(from: url)
            await MainActor.run {
                self.image = UIImage(data: data)
            }
        } catch {
            print(error.localizedDescription)
        }
    }
  }

 

 

 

 

 

[ Reference ]

-  WWDC 22 > Eliminate data races using Swift Concurrency 

-  Swiftful Thinking > Swift Concurrency 

 

 

반응형
댓글