How to use Actors and non-isolated in Swift | Swift Concurrency #9
actor-isolated
를 통해 스레드 안전성을 보장하고 있는 클래스actor
에게 데이터 패치를 요청한 각 뷰, 뷰 모델에서는 Task
내부의 await
를 통해 해당 데이터의 도착을 보장 가능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
}
}
}
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
가 경고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
}
}
}
}
}
weak self
를 통한 강한 참조 사이클을 방지 가능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
를 통해서 데이터가 요청할 때까지 기다리기MainActor
사용 → 메인 스레드 사용이 보장되는 싱글턴 액터로 구현된 애플 기본 프레임워크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
블록 외부에서도 해당 액터의 상수, 함수를 사용 가능 → 스레드 안전성이 필요없다면 해당 키워드를 사용하기