Skip to content

TaskGroup 활용과 Task-Local 변수 #9

@camosss

Description

@camosss

TaskGroup 활용

동시 실행 개수 제한

  • 큰 배열에 대해 동시에 돌릴 작업 수를 제한
  • withThrowingTaskGroup + group.next()를 이용해 n개 실행 유지
func fetchValues(_ nums: [Int], limit: Int = 2) async throws -> [Int] {

    // 자식 태스크가 Int를 반환하는 그룹 생성
    try await withThrowingTaskGroup(of: Int.self) { group in
        var it = nums.makeIterator()
        var results: [Int] = []

        // 최대 limit 만큼 먼저 addTask
        for _ in 0..<limit {
            if let n = it.next() {
                group.addTask { n * 2 }  // ex. 비동기 처리
            }
        }

        // 완료된 작업 수집 + 다음 투입
        while let value = try await group.next() {
            results.append(value)

            if let n = it.next() {
                group.addTask { n * 2 }
            }
        }
        return results
    }
}

실행

  • 항상 최대 limit개의 자식 태스크가 돌아가도록 유지
  • 끝나는 즉시 next()로 완료된 작업을 먼저 수집하고, 다음 작업을 투입
  • group.next()는 완료된 작업을 하나씩 반환(완료 순서대로)
입력: [1, 2, 3, 4], 작업: n * 2, limit = 2

시작
실행 중: {1}, {2}
results = []

1번 완료
실행 중: {2}, {3}
results = [2]

2번 완료
실행 중: {3}, {4}
results = [2, 4]

3번 완료
실행 중: {4}
results = [2, 4, 6]

4번 완료
실행 중: (없음)
results = [2, 4, 6, 8]

취소/에러 발생 시 중간 결과 반환

  • 하나라도 실패하면 나머지 작업은 자동 취소 전파
  • catch에서 지금까지 모은 결과를 돌려줄 수 있음
func fetchValuesPartial(_ nums: [Int]) async -> [Int] {
    var partial: [Int] = []
    do {
        return try await withThrowingTaskGroup(of: Int.self) { group in
            for n in nums {
                group.addTask {
                    if n == 3 { throw NSError(domain: "", code: -1) }
                    return n * 2
                }
            }

            // 완료 순서대로 방출
            for try await v in group { 
                partial.append(v) 
            }
            return partial
        }
    } catch {
        return partial  // 지금까지 수집한 값 반환
    }
}

Task-Local 변수

선언

  • @TaskLocal은 특정 Task 범위 내에서만 읽고 바인딩 가능한 타입 속성
  • 부모 Task에서 바인딩하면 자식 Task에도 값이 상속
enum DownloadOption: String {
    case basic
    case special
}

class Order {
    @TaskLocal static var option: DownloadOption = .basic
    
    func performTask() async {
        print("\(Order.option.rawValue) 다운로드 시작")

        if Order.option == .special {
            print("추가적인 작업: \(Order.option.rawValue)")
        }
    }
}

값 바인딩 (withValue)

  • withValue로 해당 블록 범위에서만 설정 -> 전역 상태 없이 컨텍스트를 전달
  • 블록이 끝나면 원래 값으로 복구
let order = Order()

Task {
    await Order.$option.withValue(.basic) {
        print("1번 화면 버튼 클릭")
        await order.performTask()
        await order.performTask()
    }

    // 블록 종료 후 기본값으로 복구 확인
    print("블록 종료 후 현재 옵션:", Order.option)  
}

Task {
    await Order.$option.withValue(.special) {
        print("2번 화면 버튼 클릭")
        await order.performTask()
        await order.performTask()
    }

    print("블록 종료 후 현재 옵션:", Order.option)
}

실행

=== 1번 화면 버튼 클릭 ===
basic 다운로드 시작
basic 다운로드 시작
블록 종료 후 현재 옵션: basic

=== 2번 화면 버튼 클릭 ===
special 다운로드 시작
추가적인 작업 실행: special
special 다운로드 시작
추가적인 작업 실행: special
블록 종료 후 현재 옵션: basic

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions