[SwiftUI] Actors

Junyoung Park·2022년 8월 29일
0

SwiftUI

목록 보기
62/136
post-thumbnail

How to use Actors and non-isolated in Swift | Swift Concurrency #9

Actors

Actor의 정의

  • actor-isolated를 통해 스레드 안전성을 보장하고 있는 클래스
  • 비동기적 데이터 패치를 사용할 때 서로 다른 요청의 순서를 보장 가능 → 해당 actor에게 데이터 패치를 요청한 각 뷰, 뷰 모델에서는 Task 내부의 await를 통해 해당 데이터의 도착을 보장 가능

비동기적 데이터 패치

데이터 패치 및 UI 뷰

struct ActorBootCamp: View {
    var body: some View {
        TabView {
            HomeView()
                .tabItem {
                    Label("Home", systemImage: "house.fill")
                }
            
            BrowseView()
                .tabItem {
                    Label("Browse", systemImage: "magnifyingglass")
                }
        }
    }
}
struct HomeView: View {
    @State private var text: String = ""
    let dataService = ActorBootCampDataServiceClass.instance
    let dataServiceActor = ActorBootCampDataServiceActor.instance
    let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
    var body: some View {
        ZStack {
            Color.gray.opacity(0.8).ignoresSafeArea()
            Text(text)
                .font(.headline)
                .fontWeight(.semibold)
        }
        .onReceive(timer) { _ in
            // DataFetch Function From DataService
        }
    }
}

struct BrowseView: View {
    @State private var text: String = ""
    let dataService = ActorBootCampDataServiceClass.instance
    let dataServiceActor = ActorBootCampDataServiceActor.instance
    let timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
    var body: some View {
        ZStack {
            Color.yellow.opacity(0.8).ignoresSafeArea()
            Text(text)
                .font(.headline)
                .fontWeight(.semibold)
        }
        .onReceive(timer) { _ in
            // DataFetch Function From DataService
        }
    }
}

문제 상황

  • 동일한 데이터 서비스 클래스 데이터 요청 시 고려 사항
  • 백그라운드 스레드를 통한 데이터 패치 + 메인 스레드를 통한 UI 패치
  • 백그라운드 스레드 → 요청한 순서대로 데이터 패치 가능 여부에 대한 스레드 안전성 이슈

데이터 서비스 클래스 코드

class ActorBootCampDataServiceClass {
    static let instance = ActorBootCampDataServiceClass()
    private init() {} // Singleton pattern
    
    var data: [String] = []
    
    func getRandomData() -> String? {
        self.data.append(UUID().uuidString)
        print("Current Thread : \(Thread.current)")
        // HomeView, BrowseView -> Same <Main Thread> (If DispatchQueue.main)
        // HomeView, BrowseView -> Different <Threads> (If DispathQueue.global)
        return data.randomElement()
    }
}

데이터 서비스 클래스 사용 코드

	private func getDataUsingMain() {
        if let data = dataService.getRandomData() {
            self.text = data
        }
    }
    
    private func getDataUsingBackgroundUnsafe() {
        // ThreadSanitizer said WARNING: ThreadSanitizer: Swift access race
        DispatchQueue.global(qos: .background).async {
            if let data = dataService.getRandomData() {
                self.text = data
            }
        }
    }
  • HomeView, BrowseView 모두 메인 스레드를 통해 데이터 서비스 접근, 데이터를 패치할 때에는 ThreadSanitizer가 경고하지 않음 → 동일한 스레드를 사용하기 때문에 스레드 안전성이 보장되기 때문, 하지만 메인 스레드를 사용해 비동기적 데이터를 패치하는 것은 낭비가 심한 방법
  • 백그라운드 스레드를 통한 데이터를 패치 → 백그라운드 스레드는 HomeView, BrowseView에서 사용하는 종류가 각각 다름 → 데이터 서비스 클래스에서 데이터 요청에 대한 순서 처리가 어렵기 때문에 스레드 안전성 보장 어려움, ThreadSanitizer가 경고

Actor 이전의 해결 방법

  • 데이터 서비스 클래스 내부의 커스텀 GCD 큐 → 서로 다른 요청을 일렬로 선입선출화 → 이스케이핑 클로저-컴플리션 핸들러를 통해 요청한 뷰 모델/뷰에 데이터 리턴

데이터 서비스 클래스 코드

class ActorBootCampDataServiceClass {
	...
    private let lock = DispatchQueue(label: "CustomQueue")
    ...
    func getRandomDataSafe(completionHandler: @escaping (_ title: String?) -> Void) {
        lock.async {
            self.data.append(UUID().uuidString)
            print("Current Thread : \(Thread.current)")
            completionHandler(self.data.randomElement())
        }
        // all of requests from multiple functions -> lined up serially
        // Must be out of Queue(lock) async block
    }
}
  • 커스텀 디스패치 큐를 생성, 데이터를 리턴하는 코드를 일렬로 세워서 스레드 안전성 보장 → 동기화를 보장하는 큐(락)
  • 큐(락) 내부에서 리턴하는 데이터는 이스케이핑 클로저를 통한 리턴 필요

데이터 서비스 클래스 사용 코드

    private func getDataUsingBackgroundSafe() {
        DispatchQueue.global(qos: .background).async {
            dataService.getRandomDataSafe { title in
                if let data = title {
                    DispatchQueue.main.async {
                        self.text = data
                    }
                }
            }
        }
    }
  • 백그라운드 스레드를 통한 데이터 패치 및 리턴받은 데이터를 UI에 그리기 위한 메인 스레드 사용
  • 추가로 메인 스레드 사용 시 weak self를 통한 강한 참조 사이클을 방지 가능

Actor 사용

  • actor 클래스 자체의 스레드 안전성 보장 → 커스텀 큐 없이 actor에 요청한 데이터 패치의 순서 보장
  • await 키워드를 통한 actor-isolated 보장

데이터 서비스 액터 코드

actor ActorBootCampDataServiceActor {
    static let instance = ActorBootCampDataServiceActor()
    private init() {}
    
    var data: [String] = []
    
    func getRandomData() -> String? {
        self.data.append(UUID().uuidString)
        print("Current Thread : \(Thread.current)")
        return self.data.randomElement()
    }
    // Easier to Code than Custom Queue made in Class
    // Await before getting to the Actor
}

데이터 서비스 액터 사용 코드

    private func getDataUsingActorSafe() {
        Task {
            if let data = await dataServiceActor.getRandomData() {
                await MainActor.run(body: {
                    self.text = data
                })
            }
        }
    }
  • Task를 통한 비동기적 async 코드 수행 가능
  • await를 통해서 데이터가 요청할 때까지 기다리기
  • 리턴받은 데이터를 UI에 그릴 때에는 MainActor 사용 → 메인 스레드 사용이 보장되는 싱글턴 액터로 구현된 애플 기본 프레임워크

Actor의 동기적 접근

  • nonisolated 키워드를 통한 스레드 안전성을 보장할 필요 없는 actor 클래스 내부의 상수 또는 함수 리턴 값을 Task 외부에서 사용 가능 → 해당 블락 내부에서는 isolated 코드 접근 불가능

데이터 서비스 액터 코드

actor ActorBootCampDataServiceActor {
	...
    nonisolated let nonisolatedValue: String = "No have to worry about Thread-Safety"
    // Call this nonisolated value without await
    ...
    nonisolated func getSavedData() -> String {
        // let data = getRandomData() -> Cannot Use
        // Actor-isolated instance method 'getRandomData()' can not be referenced from a non-isolated context
        return "No have to worry about Thread-Safety"
    }
}
  • nonisolated로 선언된 상수 및 함수는 actor-isolated가 아니기 때문에 외부에서 곧바로 접근 가능

데이터 서비스 액터 사용 코드

    private func getDataFromActor() {
        Task {
            let data = await dataServiceActor.data
            // data -> isolated from outside of actor
            print(data)
        }
        let nonisolatedFuncReturned = dataServiceActor.getSavedData()
        let nonisolatedValueReturned = dataServiceActor.nonisolatedValue
        // whether inside Task block or not, its returned value as nonisolated can be used
    }
  • Task 블록 외부에서도 해당 액터의 상수, 함수를 사용 가능 → 스레드 안전성이 필요없다면 해당 키워드를 사용하기

구현 화면

  • 서로 다른 탭뷰에서 동일한 데이터 서비스 클래스(액터)에 백그라운드 스레드를 통한 데이터 패치 요청 중
profile
JUST DO IT

0개의 댓글