Swift Concurrency: Structured Concurrency(Task)

틀틀보·2025년 4월 28일

Swift Concurency

목록 보기
2/11

비동기 작업을 실행하는 기본 단위
비동기 함수나 연산을 실행할 수 있는 컨테이너 역할

기존의 GCD

DispatchQueue.global(qos: .background).async {
    let result = fetchNetworkData()

    DispatchQueue.main.async {
        updateUI(with: result)
    }
}

func fetchNetworkData() -> String {
    // 네트워크 요청 (간단히 문자열 반환하는 예시)
    return "Fetched Data"
}

func updateUI(with data: String) {
    print("UI 업데이트: \(data)")
}

Task 방식

Task {
    let result = await fetchNetworkData()
    await updateUI(with: result)
}

func fetchNetworkData() async -> String {
    // 네트워크 요청 (간단히 문자열 반환하는 예시)
    return "Fetched Data"
}

@MainActor
func updateUI(with data: String) {
    print("UI 업데이트: \(data)")
}
  • 비동기 내부에서 스레드 이동을 직접 신경 쓸 필요가 사라짐
  • 다만 특정 백그라운드 스레드를 지정해서 동작할 수 없음.

Task의 종류

Unstructured Task

부모 작업과 명시적인 관계없이 독립적으로 생성되는 비동기 작업
실행 컨텍스트와 생명주기를 자체적으로 관리

특징

  • 독립성: 생성된 Task는 부모 작업과의 관계없이 독립적으로 실행, 자체적인 생명주기를 가짐
  • 컨텍스트 상속: 생성된 위치의 일부 컨텍스트를 상속받음.
    ex) 현재 실행중인 actor의 우선순위 등과 작업에 필요한 로컬 변수를 상속받아 작업 진행
func startBackgroundTask() {
    Task {
        await longRunningOperation()
    }
    // 이 시점에서 startBackgroundTask()는 종료되지만,
    // Task는 longRunningOperation()이 완료될 때까지 계속 실행됩니다.
}

Detatched Task

최상위 비동기 작업을 생성한다.
부모 Task, 선언된 actor 컨텍스트를 전혀 상속 받지 않는다.

특징

  • 부모 자식 관계 X
  • 부모 Task가 취소, 종료되어도 영향 X
@MainActor
func fetchData() {
    Task.detached {
        let data = await fetcher.getData()
        self.models = data // 백그라운드에서 self 접근(주의)
    }
}

MainActor로 메인 스레드에서 선언되더라도 다른 스레드에서 처리

Structured Task

작업 간 부모-자식 관계를 통해 코드의 가독성과 안정성을 높이는 모델

특징

  • async letwithTaskGroup, withThrowingTaskGroup을 활용한 부모 작업 내에 자식 작업 생성
  • 자식 작업이 완료되기 전까지 부모 작업 종료X
  • 자식 작업의 오류를 부모 작업 내에서 처리 및 취소 가능

async let

func fetchData() async throws {
    async let data1 = fetchFromAPI1()
    async let data2 = fetchFromAPI2()
    
    let result1 = try await data1
    let result2 = try await data2
    
    // 결과 처리
}

fetchData라는 부모 작업 내에 data1, data2라는 변수 각각에 자식 작업을 비동기, 병렬로 실행하고 각각을 기다린다.

withTaskGroup, withThrowingTaskGroup

func processFiles(files: [String]) async throws -> [ProcessedFile] {
    return try await withThrowingTaskGroup(of: ProcessedFile.self) { group in
        for file in files {
            group.addTask {
                return try await process(file)
            }
        }
        
        var results = [ProcessedFile]()
        for try await result in group {
            results.append(result)
        }
        return results
    }
}
  • 에러를 던져야 할 경우: withThrowingTaskGroup
  • 일반적인 작업의 경우: withTaskGroup
async let과 withTaskGroup의 사용처
async let fetchData1 = fetchData(urlString: "https://someDataRandom")
async let fetchData2 = fetchData(urlString: "https://someDataRandom")
async let fetchData3 = fetchData(urlString: "https://someDataRandom")
async let fetchData4 = fetchData(urlString: "https://someDataRandom")
async let fetchData5 = fetchData(urlString: "https://someDataRandom")

let (Data1, Data2, Data3, ...) = await (try? fetchData1, try? fetchData2, ...)

위와 같이 반복된 작업을 일일히 async let으로 선언해줘야 하는 문제를

func fetchDataGroup() async -> [UIImage] {
    let urlStrings = [
        "https://someDataRandom",
        "https://someDataRandom",
        "https://someDataRandom",
        "https://someDataRandom",
        "https://someDataRandom"
    ]

    // return 해주는 게 없으니 returing 타입은 void 로 추론됨.
    let images = await withTaskGroup(of: Data?.self) { group in
        for urlString in urlStrings {
            group.addTask {
                try? await self.fetchData(urlString: urlString)
            }
        }
    }

    return []
}

withTaskGroup내에서 반복문 선언으로 간단히 해결

우선순위 지원

Task(priority: .userInitiated) {
    // 중요한 작업 (ex. 사용자 액션에 즉시 반응해야 하는 것)
    await doImportantWork()
}

Task마다 우선순위를 두어 가장 먼저 처리되어야 할 작업을 정의 할수 있다.

각 작업들은 비선점형으로 처리되며, 기존 작업보다 우선순위가 높은 작업이 들어오더라도 기존 작업을 끝내고 그 다음 처리함.

https://developer.apple.com/documentation/swift/taskpriority

TaskPriority 값의미
.high아주 빠르게 처리해야 함
.userInitiated사용자가 시작했고, 바로 결과가 필요한 경우 (ex. 버튼 클릭)
.medium기본적인 작업 처리, 특별히 빠르거나 느릴 필요 없는 작업
.utility빠른 응답은 필요 없지만 일정 수준의 성능 요구 (ex. 다운로드, 저장)
.background중요도 낮은 작업 (ex. 캐시 정리, 백업)
.low아주 낮은 우선순위 (거의 중요하지 않은 작업)

취소 가능

Task의 취소는 협력적으로 동작
개발자가 취소 신호 발신 -> Task내에서 취소 처리

1. 취소 신호 보내기

let task = Task {
    // 수행할 작업
}

task.cancel() // 작업에 취소 신호를 보냄

cancel 메서드를 활용한 Task에게 취소 신호 보내기

2. Task 내 취소 감지

Task {
    guard !Task.isCancelled else {
        // 취소되었을 때의 처리
        return
    }

    do {
        try Task.checkCancellation()
        // 작업 수행
    } catch {
        // 취소 시 발생하는 오류 처리
    }
}
  • Task.isCancelled을 활용한 취소 처리
  • Task.checkCancellation()을 활용한 취소 및 에러 처리로 활용가능

자식 작업으로의 취소 전파

Structured Task의 경우
let task = Task {
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            // 자식 작업
            try Task.checkCancellation()
            ...
        }
        // ...
    }
}
task.cancel()

Structured Task의 경우일 때 부모 작업의 취소 신호가 전파되면 그 아래에 있는 자식 작업에게까지 취소 신호가 자동으로 전파됨.

UnStructured Task, Detatched Task의 경우
func startParentTask() {
        parentTask = Task { [weak self] in
            // 자식 Task 생성
            let childTask = Task {
                for i in 1...5 {
                    try Task.checkCancellation() // 자식 Task 취소 확인
                    print("Child Task: \(i)")
                    try await Task.sleep(nanoseconds: 1_000_000_000)
                }
            }

            // 부모 Task 작업
            for i in 1...3 {
                try Task.checkCancellation() // 부모 Task 취소 확인
                print("Parent Task: \(i)")
                try await Task.sleep(nanoseconds: 500_000_000)
            }

            // 부모 Task 완료 후 자식 Task 취소 (옵션)
            childTask.cancel() // [1] 부모가 완료되면 자식도 취소
        }
    }

    func cancelAll() {
        parentTask?.cancel() // 부모 Task 취소 → 자식 Task는 자동 취소 X
    }
    
Parent Task: 1
Parent Task: 2
Parent Task: 3

일일히 취소 호출을 해줘야 함.

profile
안녕하세요! iOS 개발자입니다!

0개의 댓글