[Conference] Swift Concurrency Under the Hood

Junyoung Park·2022년 12월 31일
0

Conference

목록 보기
5/5
post-thumbnail

Swift Concurrency Under the Hood - iOS Conf SG 2022

Swift Concurrency Under the Hood

Concurrency

  • 스레드
  • GCD
  • 모던 스위프트 컨커런시

Async Await

  • 비동기 컨텍스트에서 실행되는 함수를 컴플리션을 통해 실행하지 않더라도 실행 가능하도록 지원
  • Task 내부 또는 async를 따르는 함수에서 do catch 등을 통해 await로 비동기적으로 종료되는 태스크의 실행을 캐치 가능
import SwiftUI

struct MyStorage {
    static func fetchID() async {
        print("Async Work")
    }
    
    static func fetchID() {
        print("Sync Work")
    }
}

struct ConcurrencyView: View {
    var body: some View {
        Text("Hello, World!")
            .onAppear(perform: MyStorage.fetchID)
            .task {
                await MyStorage.fetchID()
            }
    }
}
  • 태스크 모디파이어를 통해 비동기 함수 실행
Sync Work
Async Work
  • 비동기/동기 함수가 실행되는 구문 확인 가능
struct ConcurrencyView: View {
    var body: some View {
        Text("Hello, World!")
            .onAppear(perform: MyStorage.fetchID)
            .task {
               	MyStorage.fetchID()
            }
    }
}
  • 태스크 모디파이어 내부에서 await를 쓰지 않고 fetchID()를 호출했을 때 컴파일러는 동일한 이름의 동기 함수를 실행
Sync Work
Sync Work
  • 동일한 동기 함수가 실행되는 것을 확인 가능 → await 키워드 필수

Task Groups

  • 인풋이 동적으로 들어오는 비동기 리퀘스트를 병렬적으로 실행한 뒤 결과를 종합해서 리턴/사용해야 할 때 사용 가능한 방식
func loginAndAskAndConnet() {
    try await withThrowingTaskGroup(of: LoginTaskResult.self, body: { group in
        group.addTask(operation: fetchLocation)
        group.addTask(operation: askForUserPermission)
        group.addTask(priority: .high) {
            return .client(try await APIClient.connect())
        }
        
        var client: APIClient!
        for try await result in group {
            if case LoginTaskResult.client(let api) = result {
                client = api
                break
            }
        }
        let tracer = client.traceEvent
        
        group.addTask(operation: client.logUserIn)
        group.addTask {
            try await tracer(.login)
        }
        try await group.waitForAll()
        _ = try await tracer(.completedLoginSequence)
    })
}
  • 로그인 태스크에서 발생한 가능한 타입이 of를 통해 주어진 태스크 그룹
  • 세 가지 태스크가 그룹에 추가 → 위치를 가져오면서 유저의 허락을 구하고 API 클라이언트와 연결하는 작업
  • for try await를 통해 AsyncSequence를 다루면서 태스크 그룹이 리턴하는 클라이언트 api 데이터를 가져온 뒤 클라이언트의 로그인 태스크 및 이벤트 추적을 동시적으로 진행
  • group.waitForAll()을 통해 그룹 내에 추가된 모든 태스크가 종료될 때까지 보장
  • 위 컨커런트한 태스크 그룹 → 해당 태스크 종료 후에야 특정 태스크 실행 가능한 흐름일 때 도중 특정한 태스크에서 에러를 스로우한다면 → 자동으로 그룹 내 에러가 발생한 태스크 이외의 (종료되지 않은) 태스크는 자동으로 캔슬
  • 태스크 그룹에 대한 자동 스레드 할당 방법: 컨커런트 디스패치 큐인 user-initiated-qos.cooperative 스레드 풀에서 멀티 스레드를 통해 실행
  • 각 스레드 풀에서 실행되는 스레드 개수는 디바이스의 코어 개수에 따라 상이(아이폰, 맥 등)

Actors

  • 레이스 문제를 해결하는 가장 간단한 방법 중 한 가지
actor MyActor {
    private var counter = 0
    
    public func increment() {
        counter += 1
    }
}
  • 해당 increment()를 실행하는 호출 순서가 액터가 주관하는 큐에서 실행
struct MyModel {
    @MainActor func updateItems() {}
    @MainActor func clearAllItems() {}
}

struct Logger {
    @MainActor var logs: [String]
}

@MainActor struct UIHelpers {
    // members
}
  • 일반적인 액터 클래스 이외에도 해당 함수, 변수 등이 @MainActor와 같은 형식으로 선언된다면 해당 함수 실행이 곧 isolated된다는 뜻
  • 구조체, 클래스 자체를 @MainActor와 같은 형식으로 선언했다면 내부의 모든 데이터, 함수 등이 isolated되는 게 디폴트이기 때문에 그렇지 않은 대상에 `nonisolted로 별도로 선언해 주어야 함

Actors Protocol

  • 프로토콜을 통해 액터를 따를 때 인스턴스 프로퍼티로 unownedExecutor가 존재
  • 해당 익스큐터는 unowned 참조로서 해당 액터를 실행하는 익스큐터를 반환하는 프로퍼티 변수(익스큐터란 해당 액터가 주관하는 동작을 순차적으로 실행하도록 보장하는 큐)
  • 해당 익스큐터를 커스텀한다면 보다 많은 일을 할 수 있을 거라 기대

Distributed Actor

distributed actor Database {
    func allUsers() -> [User] { ... }
}

let database = try Database.resolve(id: IP("111.222.333"), using: HTTPSTransport())
let users = try await database.allUsers()
  • distributed를 사용한 액터 클래스

    해당 강의가 나온 시점에 distributed 키워드는 아직 도입되지 않은 상황이었는데, 해당 방법을 통해 액터를 사용할 때의 이점을 설명하고 있다

  • 앱 내의 여러 프로세스가 존재할 때 특정 프로세스를 외부 데이터센터에서 실행할 수 있는 방법을 모색하고 있는 듯
profile
JUST DO IT

0개의 댓글