Task์™€ TaskGroup์€ ๋ฌด์—‡์ผ๊นŒ? ๊ทธ๋ฆฌ๊ณ  Apple์ด ๋งํ•˜๋Š” Structured Concurrency๋Š” ๋ฌด์—‡์ผ๊นŒ?

Calling Asynchronous Functions in Parallel

์•ž์˜ ๊ธ€์—์„œ ๋ณด์•˜๋“ฏ์ด await ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ๋‹ค์Œ์ฝ”๋“œ๋กœ ๋„˜์–ด๊ฐ€๊ธฐ ์ „์— ํ˜ธ์ถœ์ž๋Š” ํ•ด๋‹น ์ž‘์—…์„ ๋งˆ์น˜๋Š” ๊ฒƒ์„ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.(suspend)

let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])
 
let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

์œ„์™€ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค๋ฉด, 3๊ฐœ์˜ ์‚ฌ์ง„์ด ๋‹ค์šด๋กœ๋“œ ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ๋œ๋‹ค. ์ด๋Ÿฌํ•œ ์ ‘๊ทผ ๋ฐฉ๋ฒ•์—๋Š” ๋‹จ์ ์ด ์žˆ๋Š”๋ฐ, ๋™์‹œ์— ์—ฌ๋Ÿฌ ์‚ฌ์ง„์„ ๋‹ค์šด๋กœ๋“œ ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ , ํ•œ๋ฒˆ์— ํ•˜๋‚˜์˜ ์ฝ”๋“œ๋ฅผ ๋Œ€๊ธฐํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ํ•˜๊ณ  ์‹ถ์€ ๊ฑด, 3์žฅ์˜ ์‚ฌ์ง„์„ ๋ฐ›์•„์˜ค๋Š” ํ–‰์œ„๋ฅผ ๋ณ‘๋ ฌ์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
 
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

๋™์‹œ์— ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š”, downloadPhoto ํ•จ์ˆ˜ ์•ž์— await๋ฅผ ๋ถ™์—ฌ, ํ•ด๋‹น ํ•จ์ˆ˜์˜ ๋™์ž‘์„ ๊ธฐ๋‹ค๋ฆฌ๋„๋ก ํ•˜์ง€ ์•Š๊ณ , ํ•ด๋‹น ๊ฐ’์„ ๋ฐ›์•„์ค„ ๋ณ€์ˆ˜ ์•ž์— async๋ฅผ ๋ถ™์—ฌ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค. async let ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•˜๋ฉด, ๊ฐ๊ฐ์˜ ํ–‰์œ„๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ์—์„œ ๋™์ž‘ํ•˜๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ตœ์ข…์ ์œผ๋กœ ๋ฐ›๋Š” ๊ฒฐ๊ณผ๋Š” ๋ชจ๋‘ ๋‹ด๊ฒจ์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, photos ๋ณ€์ˆ˜๋ฅผ ๋ฐ›์„ ๋•Œ ์•ž์— await๋ฅผ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•˜๋ฉด ๋œ๋‹ค.

  • ๋น„๋™๊ธฐ ํ•จ์ˆ˜์˜ ์‹คํ–‰ ๊ฒฐ๊ณผ๊ฐ€ ์ƒํ•˜์œ„ ์ฝ”๋“œ์— ์˜์กด์ ์ธ ๊ฒฝ์šฐ(๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์•„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ) await๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ๋Œ€๊ธฐํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋ฅผ ๋ณ‘๋ ฌ์ ์œผ๋กœ ๋™์ž‘ํ•˜๊ณ  ์‹ถ์„ ๊ฒฝ์šฐ async let์„ ์‚ฌ์šฉํ•˜์ž. ์ด๋Ÿด ๊ฒฝ์šฐ Parallelํ•˜๊ฒŒ ๋™์ž‘์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.
  • await, async let ๋ชจ๋‘ ๋™์ž‘ํ•˜๊ณ  ์žˆ๋Š” ์Šค๋ ˆ๋“œ๋ฅผ suspendํ•˜๊ณ  ๋‹ค๋ฅธ ์ฝ”๋“œ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์„ ํ—ˆ์šฉํ•œ๋‹ค.

Tasks and Task Groups

Task๋Š” ํ”„๋กœ๊ทธ๋žจ์˜ ํŠน์ • ๋ถ€๋ถ„์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋™์ž‘ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋Š” work์˜ ๋‹จ์œ„๋‹ค. ๋ชจ๋“  asynchronous ์ฝ”๋“œ๋Š” Task์˜ ๋ถ€๋ถ„์œผ๋กœ ์ž‘๋™ํ•œ๋‹ค. ์œ„์—์„œ ๋ณด์•˜๋˜ async let์€, ๋‚ด๋ถ€์ ์œผ๋กœ child๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๋Š” ํ–‰์œ„์™€ ๊ฐ™๋‹ค. DispatchQueue์—์„œ DispatchGroup์„ ๋งŒ๋“  ๊ฒƒ์ฒ˜๋Ÿผ Task๋„ Group์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. Task๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŠน์ • ์ฝ”๋“œ์˜ ๋™์ž‘์„ capsuleํ™” ํ•˜์—ฌ ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š”, ๋ณ‘๋ ฌ์„ฑ๊นŒ์ง€ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

Task๋Š” ์œ„๊ณ„ ์งˆ์„œ๋ฅผ ๊ฐ€์ง„๋‹ค. Task Group์•ˆ์— ์žˆ๋Š” ๊ฐ๊ฐ์˜ Task๋Š” ๊ฐ™์€ ๋ถ€๋ชจ task๋ฅผ ๊ฐ€์ง„๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ทธ ๊ฐ๊ฐ์˜ task๋„ ์ž์‹ task๋ฅผ ๊ฐ€์ง„๋‹ค. ์ด๋ ‡๊ฒŒ task๋“ค์€ ๊ต‰์žฅํžˆ ๋ช…๋ฐฑํ•œ ๊ด€๊ณ„๋ฅผ ๊ฐ€์ง€๋Š”๋ฐ, ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฅผ structured concurrency๋ผ ๋ถ€๋ฅธ๋‹ค. structured concurrency์˜ ํ•ต์‹ฌ ์•„์ด๋””์–ด๋Š”, Task๋Š” ๋ถ€๋ชจ Task์˜ scope๋ฅผ ๋ฒ—์–ด๋‚  ์ˆ˜ ์—†๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ์ด๋Š” Task Group์—๋„ ์ ์šฉ๋œ๋‹ค. (์ถ”๊ฐ€๋˜๋Š” Child Task๊ฐ€ ์ƒ์œ„ scope๋ฅผ ๋ฒ—์–ด๋‚  ์ˆ˜ ์—†๋‹ค๋Š” ์–˜๊ธฐ)

Task Group

func getFavoriteIds(for user: User) async -> [UUID] {
    return await network.fetchUserFavorites(for: user)
}
 
func fetchFavorites(user: User) async -> [Movie] {
    // fetch Ids for favorites from a remote source
    let ids = await getFavoriteIds(for: user)
 
    // load all favorites concurrently
    return await withTaskGroup(of: Movie.self) { group in
        var movies = [Movie]()
        movies.reserveCapacity(ids.count)
 
        // adding tasks to the group and fetching movies
        for id in ids {
            group.addTask {
                return await self.getMovie(withId: id)
            }
        }
 
        // grab movies as their tasks complete, and append them to the `movies` array
        for await movie in group {
            movies.append(movie)
        }
 
        return movies
    }
}

withTaskGroup(of:returning:body:)๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด taskGroup์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์ฒซ๋ฒˆ์งธ ์ธ์ž๋Š” ์ด TaskGroup์„ ํ†ตํ•ด ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฐ๊ณผ ํƒ€์ž…์„ ์ ์–ด์ค€๋‹ค. ๋‚ด๋ถ€์—์„œ๋Š” group์˜ addTask ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด Task๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ Concurrentํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋„๋ก ํ•œ๋‹ค. addTask๋ฅผ ํ†ตํ•ด ์ถ”๊ฐ€ํ•˜๋ฉด, ๊ทธ์™€ ๋™์‹œ์— concurrentํ•˜๊ฒŒ ์ˆ˜ํ–‰๋œ๋‹ค.

์—ฌ๊ธฐ์„œ ์ฃผ๋ชฉํ•  ๋งŒํ•œ ๋ถ€๋ถ„์€, addTask์‹œ weakํ•˜๊ฒŒ self๋ฅผ captureํ•˜์ง€ ์•Š์•˜๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๊ทธ ์ด์œ ๋Š”, ๋ชจ๋“  task์˜ ๋™์ž‘์„ ๋ชจ๋‘ ๊ธฐ๋‹ค๋ฆฐ ์ดํ›„์— returnํ•˜๊ธฐ ๋•Œ๋ฌธ์— self์˜ ์กด์žฌ scope๊ฐ€ withTaskGroup์œผ๋กœ ์ œํ•œ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ด ๋ถ€๋ถ„์ด ์ดํ•ด๊ฐ€์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋Š”๋ฐ, ์•„๋ž˜๋ฅผ ๊ณ„์†ํ•ด์„œ ์ฝ์–ด๋ณด์ž.

๊ทธ๋Ÿฐ๋ฐ, ์ด์ƒํ•œ ์ ์ด ์žˆ๋‹ค. concurrentํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋Š” ๊ฒฐ๊ณผ๋“ค์„ ๋ชจ๋‘ ์ˆ˜์ง‘ํ•˜์ง€๋„ ์•Š์•˜๋Š”๋ฐ ๊ทธ ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด, group์„ loop๋ฅผ ๋Œ๊ณ  ์žˆ๋‹ค. ์ด ๋•Œ AsyncSequence์—์„œ ๋ณธ for try await ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค. ์ด ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ ค๋ฉด, group์ด AsyncSequence์ด์–ด์•ผ ํ•œ๋‹ค. group์€ TaskGroup Type์ธ๋ฐ, ์‹ค์ œ๋กœ AsyncSequence์ธ์ง€ ํ™•์ธํ•ด๋ณด์ž.

/// ==== TaskGroup: AsyncSequence ----------------------------------------------
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension TaskGroup : AsyncSequence { 
    ...
}

์‹ค์ œ๋กœ ๋‚ด๋ถ€ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด, TaskGroup์ด AsyncSequence๋ฅผ ์ฑ„ํƒํ•˜๊ณ  ์žˆ์Œ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ทธ ์•„๋ž˜์— ์ ํžŒ ์ฃผ์„์„ ์ฝ์–ด๋ณด์ž.

A type that provides an iteration interface over the results of tasks added to the group.
The elements returned by this iterator appear in the order that the tasks *completed*, not in the order that those tasks were added to the task group.

๊ทธ๋ฃน์— ์ถ”๊ฐ€๋œ ์ž‘์—… ๊ฒฐ๊ณผ์— ๋Œ€ํ•ด ๋ฐ˜๋ณต ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•˜๋Š” ์œ ํ˜•์ž…๋‹ˆ๋‹ค.
์ด ๋ฐ˜๋ณต์ž์— ์˜ํ•ด ๋ฐ˜ํ™˜๋œ ์š”์†Œ๋Š” ํƒœ์Šคํฌ ๊ทธ๋ฃน์— ์ถ”๊ฐ€๋œ ์ˆœ์„œ๊ฐ€ ์•„๋‹ˆ๋ผ ํƒœ์Šคํฌ *์™„๋ฃŒ๋จ* ์ˆœ์„œ๋กœ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

This iterator terminates after all tasks have completed. After iterating over the results of each task, it's valid to make a new iterator for the task group, which you can use to iterate over the results of new tasks you add to the group.

์ด ๋ฐ˜๋ณต๊ธฐ๋Š” ๋ชจ๋“  ์ž‘์—…์ด ์™„๋ฃŒ๋œ ํ›„ ์ข…๋ฃŒ๋ฉ๋‹ˆ๋‹ค. ๊ฐ ํƒœ์Šคํฌ์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜๋ณตํ•œ ํ›„ ํƒœ์Šคํฌ ๊ทธ๋ฃน์— ๋Œ€ํ•ด ์ƒˆ ๋ฐ˜๋ณต๊ธฐ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์œ ํšจํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฐ˜๋ณต๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ทธ๋ฃน์— ์ถ”๊ฐ€ํ•˜๋Š” ์ƒˆ ํƒœ์Šคํฌ์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜๋ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฆ‰, group์— task๊ฐ€ ์ถ”๊ฐ€๋˜๋ฉด concurrentํ•˜๊ฒŒ ๋™์ž‘์ด ์ˆ˜ํ–‰๋œ๋‹ค. ์ด ๋™์ž‘์˜ ์ˆ˜ํ–‰ ๊ฒฐ๊ณผ๋Š” task๋ฅผ ์ถ”๊ฐ€๋œ ์ˆœ์„œ๋Œ€๋กœ ๋ฐ˜ํ™˜๋˜์ง€ ์•Š๋Š”๋‹ค. AsyncSequence๋Š” concurrentํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋Š” ์ฝ”๋“œ์— ๋Œ€ํ•ด **๊ฒฐ๊ณผ๊ฐ€ ๋ฐ˜ํ™˜๋œ ์ˆœ์„œ๋Œ€๋กœ iterator๊ฐ€ ๋™์ž‘ํ•˜์—ฌ ๋‹ค์Œ ์š”์†Œ๋ฅผ ๋„˜๊ฒจ์ค€๋‹ค. **

TaskGroup์ด AsyncSequence๋ฅผ ์ฑ„ํƒํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, addTask๋กœ ์ถ”๊ฐ€ํ•œ ๋™์ž‘์— ๋Œ€ํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์„ ๋•Œ๋งˆ๋‹ค for loop์—์„œ ์ฒ˜๋ฆฌํ•œ๋‹ค๋Š” ์‚ฌ์‹ค์€ ์•Œ์•˜๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ๋ชจ๋“  task๊ฐ€ ๋ชจ๋‘ ์ฒ˜๋ฆฌ๋˜์—ˆ๋‹ค๋Š” ๋˜์—ˆ๋‹ค๋Š” ๊ฒƒ์€ ์–ด๋–ป๊ฒŒ ์•„๋Š”๊ฐ€? ์ฆ‰, group์— 10๊ฐœ์˜ ์›์†Œ๊ฐ€ return ๋˜์–ด์•ผ ํ•˜๊ณ , ์ด ์›์†Œ๊ฐ€ ๋ชจ๋‘ ๋ฐ˜ํ™˜๋˜์–ด for await ๋‚ด๋ถ€ ๋™์ž‘์„ ๋ชจ๋‘ ์ฒ˜๋ฆฌํ•œ ํ›„์—์•ผ movies๊ฐ€ return ๋˜์–ด์•ผ ํ•˜๋Š”๋ฐ ์ด๋ฅผ ์–ด๋–ป๊ฒŒ ์•Œ ์ˆ˜ ์žˆ์„๊นŒ?

์ด๋Š” ์•ž์—์„œ ๋ณธ AsyncSequence์—์„œ ๊ทธ ์‹ค๋งˆ๋ฆฌ๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค. AsyncSequence๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ iterator๋ฅผ ๊ฐ–๋Š”๋ฐ, ์—ฌ๊ธฐ์„œ ๋ชจ๋“  next ์›์†Œ๋ฅผ ๋ฐ˜ํ™˜ํ–ˆ๋‹ค๋ฉด nil์„ ๋ฆฌํ„ดํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด nil์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•ด๋‹น loop๊ฐ€ ์ข…๋ฃŒ๋˜์—ˆ์Œ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•ด๋‹น ๋งํฌ์˜ How it works ์ ˆ์„ ๋ณด๋ฉด loop๋ฅผ ์–ด๋–ป๊ฒŒ compiler๊ฐ€ ์ฒ˜๋ฆฌํ•˜๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

Example

์—ฌ๊ธฐ๊นŒ์ง€ ์ฝ์œผ๋ฉด ํ˜ผ๋ž€์Šค๋Ÿฌ์šธ ์ˆ˜ ์žˆ๋‹ค. ๊ฐ„๋‹จํ•˜๊ฒŒ ์ •๋ฆฌํ•ด๋ณด๊ฒ ๋‹ค.

  • Task์™€ TaskGroup์€ ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ๊ฐ–๋Š”๋‹ค. ์ด๋ฅผ structed concurrency๋ผ ํ•œ๋‹ค.
  • structed concurrency์—์„œ ํ•˜์œ„ task๋Š” ์ƒ์œ„ task์˜ ๋™์ž‘ ์ œ์–ด๋ฅผ ๋ฐ›๋Š”๋‹ค. ๋˜ํ•œ ํ•ด๋‹น ์Šค์ฝ”ํ”„์˜ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚  ์ˆ˜ ์—†๋‹ค.
  • TaskGroup์˜ ๊ฒฝ์šฐ AsyncSequence๋ฅผ ์ฑ„ํƒํ•˜๊ณ  ์žˆ๋‹ค. addTask๋กœ ์ถ”๊ฐ€๋œ ๋…€์„๋“ค์€ ๋ฐ˜ํ™˜ ์ˆœ์„œ๋Œ€๋กœ group stream์œผ๋กœ ์ฃผ์ž…๋œ๋‹ค.
  • addTask๋กœ ์ถ”๊ฐ€๋œ Task๋“ค์ด ๋ชจ๋‘ ์ข…๋ฃŒ๋˜๋ฉด ๋‚ด๋ถ€์ ์œผ๋กœ Iterator๊ฐ€ nil์„ ๋ฐ˜ํ™˜ํ•˜๊ณ , for loop์€ ์ข…๋ฃŒ๋œ๋‹ค.
  • AsyncSeqeunce์—์„œ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ for (try) await in ๊ตฌ๋ฌธ์—์„œ break, continue ๋“ฑ์€ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•˜๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ์‹ค์ œ๋กœ ๊ทธ๋Ÿฌํ•œ์ง€ ์ฝ”๋“œ๋กœ ์‚ดํŽด๋ณด์ž.

struct Data {
    let id: Int
}
 
Task {
    let results = await withTaskGroup(of: Data.self) { group -> [Data] in
        let list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        var datum = [Data]()
 
        for number in list {
            group.addTask {
                let result = await self.doSomething(with: number)
                print("Task Completed")
                return Data(id: result)
            }
        }
        print("For loop Completed")
 
        for await data in group {
            datum.append(data)
        }
 
        return datum
    }
    print("After task group called")
    print("results: \(results)")
}
 
func doSomething(with number: Int) async -> Int {
    let randomTime = Int.random(in: 1...3)
    sleep(UInt32(randomTime))
    print("number \(number) calculated")
    return number
}

๊ฒฐ๊ณผ๋Š” ๋‹นํ˜น์Šค๋Ÿฌ์› ๋‹ค.

For loop Completed
number 1 calculated
Task Completed
number 2 calculated
Task Completed
number 3 calculated
Task Completed
number 4 calculated
Task Completed
number 5 calculated
Task Completed
number 6 calculated
Task Completed
number 7 calculated
Task Completed
number 8 calculated
Task Completed
number 9 calculated
Task Completed
number 10 calculated
Task Completed
After task group called
results: [Concurrency.Data(id: 1), Concurrency.Data(id: 2), Concurrency.Data(id: 3), Concurrency.Data(id: 4), Concurrency.Data(id: 5), Concurrency.Data(id: 6), Concurrency.Data(id: 7), Concurrency.Data(id: 8), Concurrency.Data(id: 9), Concurrency.Data(id: 10)]

task๊ฐ€ ์ถ”๊ฐ€๋œ ๊ฒฝ์šฐ, ํ•ด๋‹น ์ž‘์—… ์Šค๋ ˆ๋“œ์— sleep์„ ๊ฑธ์—ˆ๋‹ค. ์œ„์—์„œ task์˜ ๋ฐ˜ํ™˜ ์ˆœ์„œ๋กœ ๊ฒฐ๊ณผ๊ฐ€ ๋“ค์–ด๊ฐ€๊ณ , ์ด๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค๊ณ  ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—, ๋ณ€์น™์ ์œผ๋กœ ์ˆซ์ž๊ฐ€ ๋‚˜์˜ฌ ๊ฒƒ์„ ์˜ˆ์ƒํ–ˆ์œผ๋‚˜ ๊ฒฐ๊ณผ๋Š” ์ˆœ์ฐจ์ ์œผ๋กœ ๋‚˜์™”๋‹ค. ์ด๋Š” Sync OperationQueue์™€ ๊ฐ™์€ ๊ตฌ์กฐ์— task๋“ค์ด ๋“ค์–ด๊ฐ„ ๊ฒฐ๊ณผ์™€ ๊ฐ™๋‹ค. ์ด์— ๊ด€๋ จ ๋ฌธ์ œ๋ฅผ ์ฐพ์•„๋ณด์•˜๋‹ค.

TaskGroup != Parallelism, == Concurrency

Is there an equivalent of DispatchQueue.concurrentPerform() with the new async/await?์—์„œ ์‹ค๋งˆ๋ฆฌ๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

There are no parallelism APIs with Swift concurrency that you can use that have the same behaviour of concurrentPerform. 
One noteworthy distinction is that concurrentPerform in dispatch is not an asynchronous operation 
- the caller thread participates in the operation and will block until all the operations in the concurrentPerform are completed.

concurrentPerform๊ณผ ๋™์ผํ•œ ๋™์ž‘์„ ํ•˜๋Š” Swift ๋™์‹œ์„ฑ์„ ๊ฐ€์ง„ ๋ณ‘๋ ฌ API๋Š” ์—†์Šต๋‹ˆ๋‹ค. ํ•œ ๊ฐ€์ง€ ์ฃผ๋ชฉํ•  ๋งŒํ•œ ์ฐจ์ด์ ์€ concurrentPerform in dispatch๋Š” ๋น„๋™๊ธฐ ์ž‘์—…์ด ์•„๋‹ˆ๋ผ๋Š” ์ ์ž…๋‹ˆ๋‹ค. ์ฆ‰, ํ˜ธ์ถœ์ž ์Šค๋ ˆ๋“œ๊ฐ€ ์ž‘์—…์— ์ฐธ์—ฌํ•˜๊ณ  concurrentPerform์˜ ๋ชจ๋“  ์ž‘์—…์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ์ฐจ๋‹จ๋ฉ๋‹ˆ๋‹ค.

A TaskGroup might feel like a tempting solution but it provides structured concurrency not parallelism. 
The dispatch equivalent for a TaskGroup would be to queue.async a bunch of work items to a concurrent queue 
and group the work items together with a DispatchGroup. 
It does not semantically provide you with the notion that the DispatchGroup is for a parallel compute workload, 
which is what concurrentPerform does.

์ฆ‰, Task Group์€ Parallellism ์œผ๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š๊ณ  Concurrency๋ผ๋Š” ๊ฒƒ์ด๋‹ค. ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” DispatchQueue.async์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹๊ณผ ๋น„์Šทํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋œ๋‹ค๋Š” ๊ฒƒ์œผ๋กœ ํ™•์ธ๋œ๋‹ค.

์—ฌ๊ธฐ์„œ concurrentPerform์€ ๋ช…์‹œ์ ์œผ๋กœ Parallelism์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜๋‹ค. ์ด๋Ÿฌํ•œ API๋ฅผ ๋”์ด์ƒ ์ œ๊ณตํ•˜์ง€ ์•Š์œผ๋ฉฐ, ์ผ๋ฐ˜์ ์œผ๋กœ ์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” API๋Š” ๋ชจ๋‘ Concurrency๋ผ๋Š” ์ ์„ ๋ช…ํ™•ํžˆ ํ•˜๊ณ  ์žˆ๋‹ค.

๋ฌธ์ œ ํ•ด๊ฒฐ

๋ฌธ์ œ๋Š” sleep function์— ์žˆ์—ˆ๋‹ค.

struct Data {
    let id: Int
}
 
Task {
    let results = await withTaskGroup(of: Data.self) { group -> [Data] in
        let list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        var datum = [Data]()
 
        for number in list {
            group.addTask {
                let result = await self.doSomething(with: number)
                print("Task Completed")
                return Data(id: result)
            }
        }
        print("For loop Completed")
 
        for await data in group {
            datum.append(data)
        }
 
        return datum
    }
    print("After task group called")
    print("results: \(results)")
}
 
func doSomething(with number: Int) async -> Int {
    let randomTime = Int.random(in: 1...3)
    try? await Task.sleep(nanoseconds: UInt64(randomTime * 1_000_000_000)) โœ…
    print("number \(number) calculated")
    return number
}
For loop Completed
number 3 calculated
Task Completed
number 7 calculated
Task Completed
number 9 calculated
Task Completed
number 10 calculated
Task Completed
number 5 calculated
Task Completed
number 2 calculated
Task Completed
number 1 calculated
Task Completed
number 4 calculated
Task Completed
number 6 calculated
Task Completed
number 8 calculated
Task Completed
After task group called
results: [Concurrency.Data(id: 3), Concurrency.Data(id: 7), Concurrency.Data(id: 9), Concurrency.Data(id: 10), Concurrency.Data(id: 5), Concurrency.Data(id: 2), Concurrency.Data(id: 1), Concurrency.Data(id: 4), Concurrency.Data(id: 6), Concurrency.Data(id: 8)]

๊ทธ๋ ‡๋‹ค๋ฉด Thread.sleep()๊ณผ Task.sleep()์€ ๋ฌด์—‡์ด ๋‹ค๋ฅธ๊ฐ€?

Thread.sleep vs. Task.sleep

๊ฐ€์žฅ ํฐ ์ฐจ์ด๋Š”, Thread.sleep()๋Š” Thread๋ฅผ Blockํ•˜๊ณ , Task.sleep()๋Š” Thread๊ฐ€ ์•„๋‹Œ Task๋ฅผ Suspendํ•œ๋‹ค๋Š” ์ ์ด๋‹ค. Suspendํ•˜๋Š” ๊ฒฝ์šฐ ๋‹ค๋ฅธ ์ž‘์—…์˜ ๊ฒฝ์šฐ ํ•ด๋‹น Thread์—์„œ ๊ณ„์† ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ฐจ์ด์  ํ™•์ธํ•˜๊ธฐ

let start = Date.timeIntervalSinceReferenceDate
DispatchQueue.concurrentPerform(iterations: 100) { _ in
  Thread.sleep(forTimeInterval: 1)
}
let end = Date.timeIntervalSinceReferenceDate
print(String(format: "Duration: %.2fs", end-start))
 
// Duration: 9.00s

concurrentPerform ๋ฉ”์„œ๋“œ๋Š” Apple์—์„œ ์ œ๊ณตํ•˜๋Š” Parallelism API์ด๋‹ค. 100๊ฐœ์˜ ๋ฐ˜๋ณต ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜๊ณ , ๊ฐ๊ฐ์˜ thread์—์„œ 1์ดˆ๊ฐ„ sleep ํ•ด๋ณธ๋‹ค๊ณ  ํ•ด๋ณด์ž. ์•„, ๊ทธ๋ฆฌ๊ณ  ์ด concurrentPerform ๋ฉ”์„œ๋“œ๋Š” ์‹œ์Šคํ…œ์—์„œ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋Š” maximum core์ˆ˜์— ๋งž์ถฐ์„œ thread๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. (์ถœ์ฒ˜) ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ–ˆ์„ ๊ฒฝ์šฐ ๋” ๋งŽ์€ ์Šค๋ ˆ๋“œ๋ฅผ ๋งŒ๋“ค์–ด ์‹คํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค.

์œ„์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด ์•ฝ 16 * 9 = 11.1, 11๊ฐœ ์ •๋„์˜ ์Šค๋ ˆ๋“œ๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์ž‘์—…์„ ์ˆ˜ํ–‰ํ–ˆ๋‹ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

let start = Date.timeIntervalSinceReferenceDate
await withThrowingTaskGroup(of: Void.self, body: {
    for _ in 0 ..< 100 {
        $0.addTask {
            try await Task.sleep(nanoseconds: 1_000_000_000)
        }
    }
})
let end = Date.timeIntervalSinceReferenceDate
print(String(format: "Duration: %.2fs", end-start))
 
// Duration: 1.05s

์ด๋ฒˆ์—๋Š” Task์— sleep์„ ๊ฑธ์—ˆ์„ ๊ฒฝ์šฐ๋‹ค. Task์˜ ๋™์ž‘๋งŒ suspendํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

Task๋“ค์€ Concurrent Queue๋กœ ๋“ค์–ด๊ฐ€๋Š”๊ฐ€?

๋‹ค์‹œ ๊ฐ€๋‹ค๋“ฌ๊ณ  ๋ฌด์—‡์ด ๊ถ๊ธˆํ•œ์ง€ ์ •๋ฆฌํ•ด๋ณด์•˜๋‹ค.

  1. Task๋Š” Parallelํ•˜๊ฒŒ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๊ฐ€?
  2. ๊ฒฐ๊ตญ Task์—ญ์‹œ ์ด์ „์˜ GCD, OperationQueue์˜ ๋™์ž‘์„ wrapping ํ•˜๋Š” ์นœ๊ตฌ๋ผ ์ƒ๊ฐํ•œ๋‹ค.
  3. ๊ทธ๋ ‡๋‹ค๋ฉด ์ด Task๊ฐ€ ์–ด๋Š Thread, Queue์— ๋“ค์–ด๊ฐ”๋Š”์ง€ ๋””๋ฒ„๊น…์„ ํ•ด๋ณด์ž.

๊ทธ๋ž˜์„œ ์œ„์˜ ์ฝ”๋“œ์— Break Point๋ฅผ ๊ฑธ์–ด ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ์‚ดํŽด๋ณด์•˜๋‹ค. ์ด ๋•Œ ๋ณ€ํ™”ํ•˜๋Š” Thread๋ฅผ ๊ฐ„๋žตํ•˜๊ฒŒ ๋‚˜ํƒ€๋‚ด๋ณด์•˜๋‹ค.

Task {
    print("Thread: \(Thread.current.debugDescription)") โœ…
    let results = await withTaskGroup(of: Data.self) { group -> [Data] in
        let list = (1...10).map { $0 }
        var datum = [Data]()
 
        for number in list {
            group.addTask {
                print("In AddTask Thread: \(Thread.current.debugDescription)") โœ…
                let result = await self.doSomething(with: number)
                print("After doSomething Thread: \(Thread.current.debugDescription)") โœ…โœ…โœ…
                print("Task Completed")
                return Data(id: result)
            }
        }
        print("For loop Completed")
 
        for await data in group {
            datum.append(data)
        }
 
        return datum
    }
    print("After task group called")
    print("results: \(results)")
}
 
private func doSomething(with number: Int) async -> Int {
    print("In doSomething Thread: \(Thread.current.debugDescription)") โœ…
    let randomTime = Int.random(in: 1...2)
    try? await Task.sleep(nanoseconds: UInt64(randomTime * 1_000_000_000))
    print("number \(number) calculated")
    return number
}

๋จผ์ € withTaskGroup์„ ์‹คํ–‰ํ•˜๋Š” ์‹œ๊ธฐ์—๋Š” main thread ์˜€๋‹ค. ๊ทธ๋ฆฌ๊ณ  child task์˜ ๊ฒฝ์šฐ sub thread์—์„œ ์ฒ˜๋ฆฌ๋˜๊ณ  ์žˆ์—ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด task ๋‚ด๋ถ€์—์„œ ๋‹ค๋ฅธ async ํ•จ์ˆ˜์˜ ๊ฒฐ๊ณผ๋ฅผ ๋Œ€๊ธฐํ•˜๋Š” ๋ถ€๋ถ„์—์„œ ๋™์ž‘ํ•˜๋Š” thread๊ฐ€ ๋‹ฌ๋ผ์กŒ๋‹ค. ๋‚ด๋ถ€์ ์œผ๋กœ async ํ”„๋กœ์„ธ์Šค ์—ญ์‹œ ๋‹ค๋ฅธ task๋ฅผ ๋งŒ๋“ค์–ด ๋‹ค๋ฅธ thread๋กœ ๋„˜๊ฒจ์ฃผ๋Š” ํ–‰์œ„์ด๊ธฐ ๋•Œ๋ฌธ์œผ๋กœ ์ƒ๊ฐ๋œ๋‹ค.

์ด ๋•Œ, main thread๊ฐ€ ํ•œ๊ฐ€ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด ๊ณณ์œผ๋กœ ๋™์ž‘์„ ๋„˜๊ฒจ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์‹ ๊ธฐํ•œ ๊ฒƒ์€ ์ด ๋‹ค์Œ์ด์—ˆ๋Š”๋ฐ, ์ด๋ ‡๊ฒŒ await๋กœ ๋™์ž‘์ด ์™„๋ฃŒ๋˜์–ด ๊ฒฐ๊ณผ๊ฐ€ ๋ฐ˜ํ™˜๋˜์—ˆ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  doSomething์„ ์ฒ˜๋ฆฌํ•œ ํ›„ ๋Œ์•„์™”์„ ๋•Œ์˜ Thread๋Š” sub thread๋กœ ๋Œ์•„์˜ค์ง€ ์•Š์•˜๋‹ค. (โœ…โœ…โœ… ํ‘œ์‹œ) ์ด ๋ถ€๋ถ„์€ ๋‚ด๋ถ€์ ์œผ๋กœ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ํŒŒ์•…ํ•˜๊ธฐ ์–ด๋ ค์› ๋‹ค.

Concurrency๋Š” ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”๊ฐ€

์œ„์˜ ์ž‘์—…์„ ํ•˜๋ฉด์„œ, ์•Œ๊ฒŒ๋œ ์‚ฌ์‹ค์€ TaskGroup์˜ ๊ฒฝ์šฐ Serial Queue์—์„œ ๋™์ž‘ํ•œ๋‹ค๋Š” ์‚ฌ์‹ค์ด๋‹ค. Concurrent Queue๊ฐ€ ์•„๋‹ˆ๋‹ค. ์ฆ‰ Parallel์ด ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค.

์ฆ‰, ์œ„์™€ ๊ฐ™์ด ๋™์ž‘ํ•œ๋‹ค.

ํ•ด๋‹น ๊ทธ๋ฆผ์€ WWDC 2017์˜ Modernizing Grand Central Dispatch Usage ์— ์žˆ๋˜ ๊ทธ๋ฆผ์ด๋ผ๊ณ  ํ•œ๋‹ค. ์ฐพ์•„๋ณด๋‹ˆ ํ•ด๋‹น ์˜์ƒ์ด ์‚ญ์ œ๋œ ๊ฒƒ์ธ์ง€ ํ™•์ธํ•  ์ˆ˜ ์—†์—ˆ๋‹ค. GCD ์ œ๋Œ€๋กœ ์“ฐ๊ธฐ ๊ธ€์„ ์ฐธ๊ณ ํ•˜๋ฉด ๋” ์ž์„ธํ•œ ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๋งˆ๋ฌด๋ฆฌ

ํ—˜๋‚œํ–ˆ์ง€๋งŒ, ์•ž์—์„œ ์ •๋ฆฌํ•œ ๋‚ด์šฉ์„ ๋ฒ—์–ด๋‚˜์ง€๋Š” ์•Š์•˜๋‹ค.

  • Task์™€ TaskGroup์€ ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ๊ฐ–๋Š”๋‹ค. ์ด๋ฅผ structed concurrency๋ผ ํ•œ๋‹ค.
  • structed concurrency์—์„œ ํ•˜์œ„ task๋Š” ์ƒ์œ„ task์˜ ๋™์ž‘ ์ œ์–ด๋ฅผ ๋ฐ›๋Š”๋‹ค. ๋˜ํ•œ ํ•ด๋‹น ์Šค์ฝ”ํ”„์˜ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚  ์ˆ˜ ์—†๋‹ค.
  • TaskGroup์˜ ๊ฒฝ์šฐ AsyncSequence๋ฅผ ์ฑ„ํƒํ•˜๊ณ  ์žˆ๋‹ค. addTask๋กœ ์ถ”๊ฐ€๋œ ๋…€์„๋“ค์€ ๋ฐ˜ํ™˜ ์ˆœ์„œ๋Œ€๋กœ group stream์œผ๋กœ ์ฃผ์ž…๋œ๋‹ค.
  • addTask๋กœ ์ถ”๊ฐ€๋œ Task๋“ค์ด ๋ชจ๋‘ ์ข…๋ฃŒ๋˜๋ฉด ๋‚ด๋ถ€์ ์œผ๋กœ Iterator๊ฐ€ nil์„ ๋ฐ˜ํ™˜ํ•˜๊ณ , for loop์€ ์ข…๋ฃŒ๋œ๋‹ค.
  • AsyncSeqeunce์—์„œ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ for (try) await in ๊ตฌ๋ฌธ์—์„œ break, continue ๋“ฑ์€ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•˜๋‹ค.
  • Task๋Š” Concurrency๋ฅผ ๋งŒ์กฑํ•œ๋‹ค. Parallelism์„ ๋งŒ์กฑํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ฆ‰, ํŠน์ • ์—ฐ์‚ฐ ์ฃผ์ฒด(์ฝ”์–ด)๋ฅผ ๋น ๋ฅด๊ฒŒ ๋ฒˆ๊ฐˆ์•„๊ฐ€๋ฉด์„œ ์ฒ˜๋ฆฌํ•œ๋‹ค.
  • concurrentPerform๊ณผ ๊ฐ™์€ API๋Š” ์—†๋‹ค. (๋ณ‘๋ ฌ ์—ฐ์‚ฐ)
  • ๋ณ‘๋ ฌ ์—ฐ์‚ฐ์„ ํ•˜๋ ค๋ฉด async let์„ ์‚ฌ์šฉํ•˜์—ฌ ๋…๋ฆฝ์ ์ธ Task๋ฅผ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์œผ๋กœ์„œ ๊ฐ€๋Šฅ์ผ€ ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • Thread.sleep()์€ Blocking, Task.sleep()์€ Suspend์ด๋‹ค.

Apple์ด ๋งํ•˜๋Š” Structued Concurrency๋Š” Task๊ฐ€ ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ์ด๋ฃจ๊ณ , ํ•˜์œ„ Task๋Š” ์ƒ์œ„ Task๋ฅผ ๋ฒ—์–ด๋‚  ์ˆ˜ ์—†๋‹ค๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ ์•„์ด๋””์–ด์ด๋‹ค. TaskGroup์— addTask๋ฅผ ํ•˜๊ฒŒ ๋˜๋ฉด ํŠน์ • ์Šค๋ ˆ๋“œ์˜ serial queue์— ๋“ค์–ด๊ฐ„ ๋’ค concurrentํ•˜๊ฒŒ ๋™์ž‘ํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ๋‚ด๋†“๋Š”๋‹ค. group์€ asyncSequence๋ฅผ ์ฑ„ํƒํ•˜๊ณ  ์žˆ์–ด ๋ชจ๋“  sub task์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์€ ๋’ค์— for await in loop๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

Reference