티스토리 뷰

728x90
반응형

[1] TaskGroup과 ThrowingTaskGroup

 

TaskGroup 

: A group that contains dynamically created child tasks.  (자세한 설명은 이 글 5번 참고)

 

taskGroup을 만드려면 

withTaskGroup(of:returning:body:) 를 호출.

 

 

ThrowingTaskGroup

: A group that contains throwing, dynamically created child tasks.

 

throwing task group을 만드려면

withThrowingTaskGroup(of:returning:body:) 를 호출

 

 

[2] 사용예제 

 

2.1 간단한 병렬 실행을 위해 async-let syntax 를 쓰다가 확장이 필요할 때

 

여러장의 이미지를 병렬로 fetch 해와야하는 상황이라고 해봅시다. 

저는 성공한 케이스들만 UIImage 배열로 리턴해줄 것 입니다. 

 

이미지 fetch 메소드는 아래와 같고 (여기서 코드참고했어요) 

private func fetchImage(urlString: String) async throws -> UIImage {
    guard let url = URL(string: urlString) else {
        throw URLError(.badURL)
    }

    do {
        let (data, _) = try await URLSession.shared.data(from: url)
        if let image = UIImage(data: data) {
            return image
        } else {
            throw URLError(.badURL)
        }
    } catch {
        throw error
    }
}

 

예를들어 2개의 연산 정도 되는 간단한 병렬 실행이면 async let 쓰겠지만..

func fetchImages() async -> [UIImage] {
    async let fetchImage1 = fetchImage(urlString: "https://picsum.photos/300")
    async let fetchImage2 = fetchImage(urlString: "https://picsum.photos/300")

    let (image1, image2) = await (try? fetchImage1, try? fetchImage2)
    return [image1, image2].compactMap { $0 }
}

 

이미지 여러개를 가져오도록 확장되었을 때는 async-let syntax 를 쓰기 힘들어집니다;;; 

async let fetchImage1 = fetchImage(urlString: "https://picsum.photos/300")
async let fetchImage2 = fetchImage(urlString: "https://picsum.photos/300")
async let fetchImage3 = fetchImage(urlString: "https://picsum.photos/300")
async let fetchImage4 = fetchImage(urlString: "https://picsum.photos/300")
async let fetchImage5 = fetchImage(urlString: "https://picsum.photos/300")

let (image1, image2, image3, ...) = await (try? fetchImage1, try? fetchImage2, ...)

 

이 때 TaskGroup을 씁니다. 

async-let syntax 도 내부적으로 child task 를 생성하는 데,  이와 동일하게 직접 child task를 생성하는 셈입니다. 

 

에러를 던지지 않을 것이니까 withTaskGroup을 사용해줍니다. 

메소드 시그니쳐가 좀 복잡한데 두개만 잘 체크해주면 됩니다. 

- of 는 ChildTask 의 결과 타입 
- body는 group -> GroupResult 

(returing 은 작성해도 되지만, 컴파일러에 의해 잘 추론됨) 

func withTaskGroup<ChildTaskResult, GroupResult>(
    of childTaskResultType: ChildTaskResult.Type,
    returning returnType: GroupResult.Type = GroupResult.self,
    body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
) async -> GroupResult where ChildTaskResult : Sendable

 

먼저 withTaskGroup 메소드로 TaskGroup을 만들고 

addTask(priority:operation:) 로 group 에 childTask 를 추가합니다. 

func fetchImages() async -> [UIImage] {
    let urlStrings = [
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300"
    ]

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

    return []
}

 

이제 GroupResult (이미지 배열) 를 리턴해주는 코드를 추가합니다. 

func fetchImages() async -> [UIImage] {
    let urlStrings = [
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300"
    ]

    // [UIImage] 를 리턴하므로 returing 타입은 [UIImage] 로 추론됨.
    let images = await withTaskGroup(of: UIImage?.self) { group in
        for urlString in urlStrings {
            group.addTask {
                try? await self.fetchImage(urlString: urlString)
            }
        }

        var images: [UIImage] = []
        for await image in group {
            if let image {
                images.append(image)
            }
        }
        return images
    }

    return images
}

 

 

참고로 TaskGroup은 AsyncSequence 를 conform 하고 있기 때문에

reduce 를 사용해서 간결하게 코드를 작성할 수도 있습니다. 

func fetchImages() async -> [UIImage] {
    let urlStrings = [
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300"
    ]

    let images = await withTaskGroup(of: UIImage?.self) { group in
        for urlString in urlStrings {
            group.addTask {
                try? await self.fetchImage(urlString: urlString)
            }
        }

        return await group
            .reduce(into: [UIImage?](), { $0.append($1) })
            .compactMap { $0 }
    }

    return images
}

 

최종코드

func fetchImages() async -> [UIImage] {
    let urlStrings = [
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300"
    ]

    return await withTaskGroup(of: UIImage?.self) { group in
        for urlString in urlStrings {
            group.addTask {
                try? await self.fetchImage(urlString: urlString)
            }
        }

        return await group
            .reduce(into: [UIImage?](), { $0.append($1) })
            .compactMap { $0 }
    }
}

 

 

[3] Child Task 의 결과에 즉각 반응하기

아래 내용은 'Modern Concurrency in Swift (chapter 7)' 에 나오는 내용을 참고했습니다.

 

TaskGroup 은 동적으로 group 의 workload 를 매니징할 수 있게 해줍니다. 

즉 실행 중에 새로운 task 를 추가하거나 취소하는 것이 가능하고

각 task 의 결과에 따른  즉각적인 업데이트도 가능합니다. 

(예를들어 progress UI 를 점진적으로 업데이트 해야한다던가.. 각 task 의 결과를 보고 group execution 을 컨트롤해야한다던가.. )

 

체크한 부분에 해당 코드를 추가하면 됩니다. 

예를들어 각 작업의 진행상황을 즉각적으로 알 수 있는 코드 입니다. 

func fetchImages() async {
    let urlStrings = [
        "https://picsum.photos/100",
        "https://picsum.photos/200",
        "https://picsum.photos/300",
        "https://picsum.photos/400",
        "https://picsum.photos/500"
    ]

    await withTaskGroup(of: UIImage?.self) { group in
        for urlString in urlStrings {
            group.addTask {
                try? await self.fetchImage(urlString: urlString)
            }
        }

        ✅ 
        for await image in group {
            print("Completed:", image)
        }

        print("Done.")
    }
}


// 출력결과
Completed: Optional(<UIImage:0x6000033100a0 anonymous {200, 200} renderingMode=automatic(original)>)
Completed: Optional(<UIImage:0x6000033140a0 anonymous {100, 100} renderingMode=automatic(original)>)
Completed: Optional(<UIImage:0x600003301040 anonymous {300, 300} renderingMode=automatic(original)>)
Completed: Optional(<UIImage:0x600003301040 anonymous {400, 400} renderingMode=automatic(original)>)
Completed: Optional(<UIImage:0x600003309860 anonymous {500, 500} renderingMode=automatic(original)>)
Done.

 

상황에 따라 이 쪽에 addTask 나 cancel 코드를 넣을 수도 있겠습니다.

 

[4] TaskGroup 의 Cancellation behavior

TaskGroup 은 위에서 사용하던 addTask 뿐만아니라 addTaskUnlessCancelled 메소드도 제공합니다.

 

이 메소드의 필요성을 파악하기 위해, 우선 TaskGroup 이 취소되는 상황을 살펴봅시다. 

 

- cancelAll()이 호출될 때
- 이 TaskGroup 을 실행하는 Task가 취소될 때 (예를 들어 아래와 같은 상황의 Task)

let task = Task {
      await withTaskGroup(of:) { .. }
}

 

(참고로 TaskGroup은 structured concurrency 에 해당하는 타입이므로, TaskGroup 을 취소하면 모든 child tasks 들에게 자동으로 취소가 전파됩니다.)

 

 

취소된 TaskGroup은 여전히 Task 를 새로 추가할 수 있지만, 추가된 Task 는  즉시 취소되기 시작합니다.

이 때, 이미 취소된 TaskGroup에는 새 Task 를 추가하지 않고 싶다! 하면 조건 없이 작업을 추가하는 addTask 대신, addTaskUnlessCancelled 를 사용합니다. 

 

이 메소드는 리턴값도 있는데 task 가 그룹에 추가되었으면 true, 아니면 false 를 반환합니다. 

 

 

 

 

[ 추천 글 ] 

 

잘 정리되어있음 👍 

https://liam777.tistory.com/41

 

Concurrency (7) Concurrent Code With TaskGroup

이전글 - Concurrency(6) Testing Asynchronous Code Modern Concurrency in Swift를 읽고 간단 정리 현재 챕터까지 진행하면서 아래 내용을 학습하였다.async/await를 사용한 코드 설계Asynchronous Sequence 생성async let 바

liam777.tistory.com

 

 

 

반응형

'🍏 > Swift' 카테고리의 다른 글

[Swift] actor  (0) 2023.04.10
[Swift] Sendable  (0) 2023.04.08
[Swift] Understanding Swift Performance 를 보면서 알게 된 것  (0) 2023.03.04
[Swift Collections] deque  (1) 2022.10.08
[Swift] some, any 키워드  (2) 2022.07.16
댓글