Task๋ฅผ ์•Œ์•„๋ณด๋ฉด์„œ ๋งŽ์€ ์‚ฝ์งˆ์„ ํ–ˆ์œผ๋‹ˆ, ์ด์ œ ๋‹ค์‹œํ•œ๋ฒˆ WWDC ์˜์ƒ์„ ๋ด๋ณธ๋‹ค.

Intro

์˜ˆ์ „์˜ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด๋Š” control flow๊ฐ€ ์ƒํ•˜๋กœ ์™”๋‹ค๊ฐ”๋‹คํ–ˆ์—ˆ๋‹ค. ์ด๋Ÿฐ ์ฝ”๋“œ๋Š” ํ๋ฆ„์„ ์ฝ๋Š” ๊ฒƒ์„ ๋ฐฉํ•ดํ–ˆ๋‹ค. ํ•˜์ง€๋งŒ ์š”์ฆ˜์€ ๊ตฌ์กฐํ™”๋œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ๋ฒ•์„ ํ†ตํ•ด ์ด๋ฅผ ์‰ฝ๊ฒŒ ์ฝ์„ ์ˆ˜ ์žˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฒƒ์ด ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋œ ๊ฒƒ์€, block์„ ์‚ฌ์šฉํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. block ์•ˆ์—์„œ๋Š” ๋ณ€์ˆ˜๊ฐ€ ์‚ด์•„์žˆ๊ณ , ๊ทธ scope๋ฅผ ๋ฒ—์–ด๋‚˜๊ฒŒ ๋˜๋Š” ๊ฒฝ์šฐ ๋ณ€์ˆ˜๋Š” ์‚ฌ๋ผ์ง„๋‹ค. ์ด๋Ÿฐ static scope์™€ structured programming ๋ฐฉ๋ฒ•์€, ๋ณ€์ˆ˜์˜ life time๊ณผ ์ œ์–ด๋ฌธ์„ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์—ˆ๋‹ค.

์ด๋ ‡๊ฒŒ structured programming ๋ฐฉ์‹์€ ์ด๋ฏธ ์šฐ๋ฆฌ์—๊ฒŒ ์ƒ๋‹นํžˆ ์ต์ˆ™ํ•˜๋‹ค. ํ•˜์ง€๋งŒ ์š”์ฆ˜์˜ program์€ ๋น„๋™๊ธฐ, concurrent code๊ฐ€ ๋งŽ์•„์กŒ๋‹ค. ์ด๋Ÿฐ ๋ถ€๋ถ„์— ์žˆ์–ด์„œ structured ํ•œ ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๋งค์šฐ ์–ด๋ ค์› ๋‹ค.

Structured Concurrency

๊ทธ๋Ÿผ ๋น„๋™๊ธฐ, concurrent ์ฝ”๋“œ์— structuredํ•œ ๋ฐฉ์‹์„ ๋„์ž…ํ–ˆ์„ ๋•Œ, ์–ผ๋งˆ๋‚˜ ์ง๊ด€์ ์ธ์ง€ ํ™•์ธํ•ด๋ณด์ž.

func fetchThumbnails(for ids: [String],
                     completion handler: @escaping ([String: UIImage]?, Error?) -> Void) {
    guard let id = ids.first else {
        return handler([:], nil)
    }
 
    let request = thumbnailURLRequest(for: id)
    URLSession.shared.dataTask(with: request) { data, response, error in
        guard let response = response, let data = data else { // โŽ: Error ์ฒ˜๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Œ
            return handler(nil, error)
        }
 
        // check response...
        UIImage(data: data)?.prepareThumbnail(of: thumbSize) { image in
            guard let image = image else {
                return handler(nil, ThumbnailFailedError())
            }
 
            fetchThumbnails(for: Array(ids.dropFirst())) { thumbnails, error in // โŽ loop ์‚ฌ์šฉ ๋ถˆ๊ฐ€
                // add image..
            }
        }
    }
}
  • Error ์ฒ˜๋ฆฌ๋ผ๋Š” structured ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Œ
  • ๋„คํŠธ์›Œํฌ ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ฌ ๋•Œ, loop์™€ ๊ฐ™์€ structured ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Œ

๊ทธ๋Ÿผ async/await์„ ํ†ตํ•ด ๋ณ€๊ฒฝ๋œ ๊ฒƒ์„ ์‚ดํŽด๋ณด์ž.

func fetchThumbnails (for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UlImage] = [:]
    for id in ids {
        let request = try await thumbnailURLRequest(for: id)
        let (data, response) = try await URLSession.shared.data(for: request)
        try validateResponse(response)
        guard let image = await UIImage (data: data)?.byPreparingThumbnail (ofSize: thumbSize) else {
            throw ThumbnailFailedError()
        }
        thumbnails[id] = image
    }
    return thumbnails
}

์—ฌ๊ธฐ๊นŒ์ง€๋Š” ์ด์ „๊ธ€์—์„œ ๋ณธ async/await๊ณผ ๋™์ผํ•˜๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋งŒ์•ฝ thumbnail ์ด๋ฏธ์ง€๋ฅผ ์ˆ˜์ฒœ์žฅ ๋ฐ›์•„์•ผ ํ•œ๋‹ค๋ฉด ์ด ์ฝ”๋“œ๋Š” ์ข‹์ง€ ๋ชปํ•˜๋‹ค. await์—์„œ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๊ฐ€ ๋๋‚  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

Task

  • Code๋ฅผ Concurrentํ•˜๊ฒŒ ์‹คํ–‰์‹œํ‚ค ์œ„ํ•œ ์ƒˆ๋กœ์šด ๋น„๋™๊ธฐ ๋ฐฉ์‹
  • Tasks๋“ค์€ ํšจ์œจ์ ์ด๊ณ , ์•ˆ์ „ํ•˜๋‹ค๊ณ  ํŒ๋‹จ๋˜๋Š” ๊ฒฝ์šฐ์— ์ž๋™์œผ๋กœ Parallelํ•˜๊ฒŒ ๋™์ž‘ํ•จ
  • Task๋Š” Swift์™€ ๊นŠ๊ฒŒ ํ†ตํ•ฉ๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— compiler๊ฐ€ concurrency ๋ฒ„๊ทธ๋ฅผ ์–ด๋Š์ •๋„ ํƒ์ง€ํ•ด์คŒ
  • async function์„ ๋‹จ์ˆœํžˆ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์œผ๋กœ Task๊ฐ€ ์ƒ๊ธฐ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋‹ค. ๋ช…์‹œ์ ์œผ๋กœ Task๋‚ด๋ถ€์— ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ ๋„ฃ์–ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

Async-let tasks

๋‹จ์ˆœํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋™๊ธฐ์ ์œผ๋กœ ๋ฐ›์•„์˜ค๋Š” ๋ฐฉ์‹์„ ์ƒ๊ฐํ•ด๋ณด๋ฉด ์œ„์™€ ๊ฐ™๋‹ค.

ํ•˜์ง€๋งŒ ์šฐ๋ฆฌ๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ›์•„์˜ค๋Š” ์‹œ๊ฐ„๋™์•ˆ์— ๋‹ค๋ฅธ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‹ถ๋‹ค. ์ด๋Ÿด ๊ฒฝ์šฐ async let์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค. ์ด๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋’ค์˜ ํ˜ธ์ถœํ•˜๋Š” ํ•จ์ˆ˜(URLSession.shared.data(~))๊ฐ€ async ํ•จ์ˆ˜์—ฌ์•ผ ํ•œ๋‹ค. Concurrent Binding ํ‰๊ฐ€ ๋ฐฉ์‹์— ๋Œ€ํ•ด ์ดํ•ดํ•ด๋ณด์ž.

  1. ์ด์ „ ์ƒํƒœ์—์„œ Child Task๋ฅผ ๋งŒ๋“ ๋‹ค.
  2. Child Task์•ˆ์—์„œ async let์œผ๋กœ async ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
  3. Parent Task(์ด์ „ ์ƒํƒœ์—์„œ ์‚ฌ์šฉํ•˜๋˜ Task)๋ฅผ ์œ„ํ•ด result์— placeholder๋ฅผ ํ• ๋‹นํ•œ๋‹ค.
  4. ์‹ค์ œ ๋™์ž‘(URLSession.shared.data())์€ Child Task์—์„œ ์ˆ˜ํ–‰ํ•œ๋‹ค.
  5. Parent Task๋Š” ๋„คํŠธ์›Œํฌ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ์ง„ํ–‰ํ•œ๋‹ค.
  6. ํ•˜์ง€๋งŒ ์‹ค์ œ๋กœ Parent Task๊ฐ€ ๋‹ค์šด๋กœ๋“œ๋œ ๊ฐ’์„ ํ•„์š”๋กœ ํ•œ๋‹ค๋ฉด, await๋ฅผ ํ†ตํ•ด child Task์˜ ๋™์ž‘์„ ๋Œ€๊ธฐํ•  ์ˆ˜ ์žˆ๋‹ค.
  7. ๋งŒ์•ฝ Error๋ฅผ ๋˜์ง€๋Š” async ํ•จ์ˆ˜๋ผ๋ฉด, try๋ฅผ ํ†ตํ•ด ๋ฐ›์•„์ฃผ๋ฉด ๋œ๋‹ค.

Apply to Thumbnail fetching code

func fetchOneThumbnail(withId id: String) async throws -> UIImage {
    let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id)
    let (data, _) = try await URLSession.shared.data(for: imageReq) โœ…
    let (metadata, _) = try await URLSession.shared.data(for: metadataReq) โœ…
 
    guard let size = parseSize(from: metadata), โœ…
          let image = await UIImage(data: data)?.byPreparingThumbnail(ofSize: size) else { โœ…
            throw ThumbnailFailedError()
          }
    return image
}
 
func fetchOneThumbnail(withId id: String) async throws -> UIImage {
    let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id)
    async let (data, _) = URLSession.shared.data(for: imageReq) // โœ…: Child Task๊ฐ€ ์ƒ์„ฑ๋จ
    async let (metadata, _) = URLSession.shared.data(for: metadataReq) // โœ…: Child Task๊ฐ€ ์ƒ์„ฑ๋จ
 
    guard let size = parseSize(from: try await metadata), โœ…
          let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size) else { โœ…
            throw ThumbnailFailedError()
          }
    return image
}

async let์„ ์‚ฌ์šฉํ•˜์—ฌ, ๊ฐ task์˜ ๋™์ž‘์„ ์‹ค์ œ ๋ฐ›๋Š” ๊ณณ์—์„œ ๋Œ€๊ธฐํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ๋‹ค. ์œ„์—์„œ ์„ค๋ช…ํ•œ ๋ฐ”์™€ ๊ฐ™์ด, async let์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐ์ดํ„ฐ์˜ ํ• ๋‹น๊นŒ์ง€ ๋Œ€๊ธฐํ•˜์ง€ ์•Š๊ณ , ์‹ค์ œ ์‚ฌ์šฉํ•˜๋Š” ์‹œ์ ์— ๋Œ€๊ธฐํ•˜์—ฌ ๋ฐ›๋Š” ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ ๋ณด๋‹ค ํšจ์œจ์ ์ธ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.

Task Tree

async let์„ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด, ๊ฒฐ๊ตญ์— compiler๋Š” Child task๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋œ๋‹ค. ์ด ๊ณผ์ •์—์„œ Child task๋“ค์€ Task Tree๋ผ๋Š” ์œ„๊ณ„ ์งˆ์„œ์˜ ํ•œ ๋ถ€๋ถ„์ด๋‹ค.

์ด Task Tree๋Š” Structured Concurrency์—์„œ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด๋‹ค. ์ด Tree๋Š” ๋‹จ์ˆœํžˆ ๊ตฌํ˜„์„ ์œ„ํ•ด ์กด์žฌํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ฉฐ, cancellation, priority, task-local ๋ณ€์ˆ˜๋“ค์— ์˜ํ–ฅ์„ ๋ฏธ์นœ๋‹ค.

async let์™€ ๊ฐ™์€ structured task๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด, ํ˜„์žฌ ๋™์ž‘ํ•˜๊ณ  ์žˆ๋Š” function์˜ task์˜ child๊ฐ€ ๋˜์–ด ๋™์ž‘ํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด child task์˜ life cycle์€ parent์˜ scope์— ๊ฐ‡ํžŒ๋‹ค.

Parent Task๋Š” ๋ณธ์ธ์ด ๊ฐ€์ง„ Child Task๋“ค์˜ ๋™์ž‘์ด ๋ชจ๋‘ ์ข…๋ฃŒ๋˜์–ด์•ผ ๋น„๋กœ์†Œ ์ข…๋ฃŒ๋  ์ˆ˜ ์žˆ๋‹ค. ์ด ๊ทœ์น™์€ โ€œ๋น„์ •์ƒ์ ์ธ ์ œ์–ด ํ๋ฆ„โ€์—๋„ ์ ์šฉ๋˜์–ด ํ•˜์œ„ ์ž‘์—…์ด ๋Œ€๊ธฐํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•œ๋‹ค. ๋น„์ •์ƒ์ ์ธ ์ œ์–ด ํ๋ฆ„์„ ์‚ดํŽด๋ณด์ž.

Cancellation Propagates

func fetchOneThumbnail(withId id: String) async throws -> UIImage {
    let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id)
    async let (data, _) = URLSession.shared.data(for: imageReq) 
    async let (metadata, _) = URLSession.shared.data(for: metadataReq) // โ“ 
 
    guard let size = parseSize(from: try await metadata), // ๐Ÿ’ฃ Error ๋ฐœ์ƒ
          let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size) else {
            throw ThumbnailFailedError()
          }
    return image
}

์ด ์ฝ”๋“œ์—์„œ๋Š” data๋ฅผ ๋„ฃ๊ธฐ ์ „์—, metadata๋ฅผ ๋จผ์ € awaitํ•˜๊ณ  ์žˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ, ์ด ๋‹จ๊ณ„์—์„œ Error๋ฅผ ๋˜์ง„๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ? ์ผ๋‹จ์€ ํ•ด๋‹น ํ•จ์ˆ˜๊ฐ€ ๋น„์ •์ƒ์ ์ธ ๋™์ž‘์„ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ”๋กœ throw๋ฅผ ํ•˜๊ณ  ์ข…๋ฃŒํ•˜๋Š” ๊ฒƒ์ด ๋งž๋‹ค.

๊ทธ๋Ÿฐ๋ฐ, ์œ„์—์„œ โ“์€ ์—ฌ์ „ํžˆ ๋™์ž‘ํ•˜๊ณ  ์žˆ๋‹ค. Parent Task๋Š” ๋ณธ์ธ์ด ๊ฐ€์ง„ Child Task๋“ค์˜ ๋™์ž‘์ด ๋ชจ๋‘ ์ข…๋ฃŒ๋˜์–ด์•ผ ๋น„๋กœ์†Œ ์ข…๋ฃŒ๋  ์ˆ˜ ์žˆ๋‹ค. ๋ผ๋Š” ๊ทœ์น™์€ Task Tree์—์„œ ๋ชจ๋‘ ์ ์šฉ๋˜๊ธฐ ๋•Œ๋ฌธ์—, ์ตœ์•…์˜ ๊ฒฝ์šฐ data๋ฅผ ๋ฐ›์„ ์ˆ˜ ์—†๋‹ค๋ฉด ๋ฌดํ•œ์ • ๋Œ€๊ธฐํ•˜๋Š” ์ƒํ™ฉ์ด ํŽผ์ณ์งˆ ์ˆ˜๋„ ์žˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋น„์ •์ƒ ์ ์ธ exit์— ๋Œ€ํ•ด Swift๋Š” ์ž๋™์ ์œผ๋กœ ๋Œ€๊ธฐํ•˜์ง€ ์•Š์€ task(data)๋ฅผ canceled๋กœ ๋งˆํ‚นํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํ•จ์ˆ˜๋ฅผ ํƒˆ์ถœํ•˜๊ธฐ ์ „์— cancel๋œ task๋ฅผ ๊ธฐ๋‹ค๋ฆฐ๋‹ค. ์—ฅ ์ด๊ฒŒ ๋ฌด์Šจ๋ง์ธ๊ฐ€.

cencel๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ๊ณผ task๋ฅผ stopํ•˜๋Š” ๊ฒƒ์€ ๋™์น˜๊ฐ€ ์•„๋‹ˆ๋‹ค. cancelํ•œ๋‹ค๋Š” ๊ฒƒ์€ Task์—๊ฒŒ โ€œ์•ผ์•ผ, ๋‹ˆ๊ฐ€ ๊ฒฐ๊ณผ ๋ฐ›์•„์™€๋„ ๊ทธ๊ฑฐ ๋‚˜ ์•ˆ์“ธ๊ฑฐ์•ผโ€๋ผ๊ณ  ๋งํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™๋‹ค. ์‹ค์ œ๋กœ๋Š” task๊ฐ€ canceled๋˜๋ฉด, cancel ๋ช…๋ น์„ ๋ฐ›์€ task์˜ ๋ชจ๋“  subtask๋“ค์ด ์ž๋™์ ์œผ๋กœ cancel๋œ๋‹ค. ์ฆ‰, propagate๋œ๋‹ค๋Š” ๋ง์ด๋‹ค.

๊ฐ€์žฅ ํ•˜์œ„์— ์žˆ๋Š” task๋ถ€ํ„ฐ cancel๋˜์–ด finish ํŒ์ •์„ ๋ฐ›์œผ๋ฉด, ์ƒ์œ„๋กœ ๊ฒฐ๊ณผ๊ฐ€ ์˜ฌ๋ผ์˜จ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ์ตœ์ข…์ ์œผ๋กœ fetchOneThumbnail ํ•จ์ˆ˜๊ฐ€ ์ข…๋ฃŒ๋œ๋‹ค.

์ด ์•Œ๊ณ ๋ฆฌ์ฆ˜์ด structured concurrency์˜ ๊ทผ๋ณธ์ด๋‹ค. ์ด๋ ‡๊ฒŒ ๋นก๋นกํ•˜๊ฒŒ ์งœ๋†“์•˜๊ธฐ ๋•Œ๋ฌธ์—, ARC๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์˜ ์ˆ˜๋ช…์„ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ task์˜ life cycle์ด ์ƒˆ๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•œ๋‹ค.

์ •๋ฆฌํ•ด๋ณด์ž.

  • Task๋Š” cancelled์‹œ์— ์ฆ‰์‹œ Stopํ•˜์ง€ ์•Š๋Š”๋‹ค.
  • SubTask๋“ค์—๊ฒŒ Cancel๋ช…๋ น์ด ์ „ํŒŒ๋œ๋‹ค.
  • ์ด๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋“œ์—์„œ ๋ช…์‹œ์ ์œผ๋กœ cancellation์— ๋Œ€ํ•ด ์ฒดํฌํ•˜๊ณ , ์ ์ ˆํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ์‹คํ–‰์„ ์ค‘์ง€ํ•ด์•ผ ํ•œ๋‹ค. ์ด๋Š” ์ฝ”๋“œ์งค ๋•Œ Cancel์— ๋Œ€ํ•ด ํ•ญ์ƒ ์ˆ™์ง€ํ•˜๊ณ  ์žˆ์–ด์•ผ ํ•œ๋‹ค๋Š” ๋ง์ด๋‹ค.

Task Cancellation

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    for id in ids {
        try Task.checkCancellation() // โœ… Cancel ๋˜์—ˆ๋‹ค๋ฉด Error๋ฅผ ๋˜์ง„๋‹ค.
        thumbnails[id] = try await fetchOneThumbnail(withID: id)
    }
    return thumbnails
}

์ด๋ฒˆ์—๋Š” ํ•˜๋‚˜์˜ Thumbnail๋งŒ ๋ฐ›๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๊ณ , ๋ชจ๋“  Thumbnail์„ ๋ฐ›์•„์˜ค๋„๋ก ํ•จ์ˆ˜๋ฅผ ๊ตฌ์„ฑํ–ˆ๋‹ค. ํ•ด๋‹น ํ•จ์ˆ˜๊ฐ€ ํŠน์ • Task ๋‚ด๋ถ€์—์„œ ๋ถˆ๋ ธ๊ณ , ์ด Task๊ฐ€ cancel๋˜์—ˆ๋‹ค๋ฉด, ์šฐ๋ฆฌ๋Š” ๋”์ด์ƒ ํ•„์š”์—†๋Š” thumbnail์„ ๋ฐ›๊ณ  ์‹ถ์ง€ ์•Š์„ ๊ฒƒ์ด๋‹ค. ๊ทธ๋ž˜์„œ loop ๋ฌธ ์•ˆ์— Task.checkCancellation()ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ cancel๋˜์—ˆ์„ ์‹œ error๋ฅผ ๋˜์น˜๊ฒŒ ํ–ˆ๋‹ค.

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    for id in ids {
        if Task.isCancelled { break } // โœ…
        thumbnails[id] = try await fetchOneThumbnail(withID: id)
    }
    return thumbnails
}

ํ˜น์€ cancel ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•˜์—ฌ loop๋ฌธ์„ ํƒˆ์ถœํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๋‹ค. ์ด๋ ‡๊ฒŒํ•˜๋ฉด, ๋ถ€๋ถ„์ ์œผ๋กœ ๋ฐœ์ƒํ•œ ๊ฒฐ๊ณผ๋งŒ returnํ•  ์ˆ˜ ์žˆ๋‹ค. ๋งŒ์•ฝ ์ด๋ ‡๊ฒŒ ์ฒ˜๋ฆฌํ•œ๋‹ค๋ฉด, ์‚ฌ์šฉํ•˜๋Š”์ชฝ์—์„œ ์ผ๋ถ€ ๊ฒฐ๊ณผ๋งŒ return๋  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ™•์‹คํžˆ ์•Œ๊ณ  ์žˆ์–ด์•ผ ํ•œ๋‹ค. ๋งŒ์•ฝ ๊ทธ๋ ‡๊ฒŒ ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ์‚ฌ์šฉํ•˜๋Š” ์ชฝ์—์„œ ์™„์„ฑ๋œ result๋งŒ ๋ฐ›์„ ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ•˜์—ฌ fatalError๊ฐ€ ๋‚  ์ˆ˜ ์žˆ๋‹ค.

Group Tasks

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    for id in ids {
        thumbnails[id] = try await fetchOneThumbnail(withID: id) // โœ…
    }
    return thumbnails
}
 
func fetchOneThumbnail(withId id: String) async throws -> UIImage {
    let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id)
    async let (data, _) = URLSession.shared.data(for: imageReq)
    async let (metadata, _) = URLSession.shared.data(for: metadataReq)
 
    guard let size = parseSize(from: try await metadata), // โœ…
          let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size) else { // โœ…
            throw ThumbnailFailedError()
          }
    return image
}

fetchThumbnails ํ•จ์ˆ˜์—์„œ๋Š” ids๋ฅผ ๋Œ๋ฉด์„œ ํ•˜๋‚˜์˜ Thumbnail์„ ๊ฐ€์ ธ์˜จ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ทธ ์•ˆ์—์„œ async let ๊ตฌ๋ฌธ์„ ํ†ตํ•ด์„œ ๋‘๊ฐœ์˜ Child Task๋ฅผ ๋งŒ๋“ค๊ณ , ์ด Child Task์˜ ๋ชจ๋“  ๋™์ž‘์ด ์™„์„ฑ๋œ ๊ฒฝ์šฐ returnํ•˜์—ฌ thumbnails[id]์— ๋ฐ˜์˜๋œ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ, ์ด๋ ‡๊ฒŒ ๋˜๋ฉด Concurrency๋ฅผ ์ œ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์ง€ ๋ชปํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ์ด๋‹ค. ํ•˜๋‚˜์˜ thumbnail์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์€ ๋ถ„๋ช… ๋…๋ฆฝ์ ์ธ Task์ธ๋ฐ, for loop์„ ๋Œ๋ฉด์„œ ํ•ด๋‹น ์ž‘์—…์„ awaitํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ์ด ์—ญ์‹œ๋„ Concurrentํ•˜๊ฒŒ ๋™์ž‘ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์„๊นŒ?

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    try await withThrowingTaskGroup(of: Void.self) { group in
        for id in ids {
            group.addTask {
                thumbnails[id] = try await fetchOneThumbnail(withID: id) // โœ…
            }
            
        }
    }
    return thumbnails
}

์—ฌ๊ธฐ์„œ ์ด์ „ ๊ธ€์—์„œ ๋ฐฐ์› ๋˜ TaskGroup์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค. addTask ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๋™์ž‘ํ•˜๋Š” scope๋ฅผ Task๋กœ ๋„ฃ๊ฒŒ๋˜๋ฉด, Concurrentํ•˜๊ฒŒ ๋™์ž‘ํ•œ๋‹ค.

ํ•˜์ง€๋งŒ, ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. Compiler๊ฐ€ data race issue๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์นœํžˆ ์•Œ๋ ค์ค€๋‹ค. ์ฆ‰, ๊ณต์œ  ์ž์›์— ์ ‘๊ทผํ•˜๊ณ  ์žˆ์–ด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. data race ์ƒํƒœ๋ฅผ Compiler๊ฐ€ ์ฒดํฌํ•ด์ค€๋‹ค.

Data-race Safety

Task๋ฅผ ๋งŒ๋“ค ๋•Œ๋งˆ๋‹ค, Task๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” Work๋Š” ์ƒˆ๋กœ์šด Closure type์ธ @Sendable Closure ์ด๋‹ค. @Sendable closure์˜ Body๋Š” lexical context ์•ˆ์—์„œ mutable variable์„ captuingํ•˜๋Š” ๊ฒƒ์„ ์ œํ•œํ•œ๋‹ค. ์™œ๋ƒํ•˜๋ฉด, Task๊ฐ€ ์‹คํ–‰๋˜๋Š” ๋™์•ˆ capturing๋œ ๋ณ€์ˆ˜๋“ค์ด ๋ณ€ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. (๊ทธ๋ƒฅ ํŠน์ • ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ด์„œ ๊ฒฐ๊ณผ๋งŒ ๋‚˜์˜ค๋Š” ๊ฒฝ์šฐ๋Š” ๋ฌธ์ œ๊ฐ€ ์—†๋‹ค.)

๊ทธ๋ ‡๋‹ค๋ฉด, Task์•ˆ์— ๋„ฃ๋Š” ๊ฐ’๋“ค์€, ๊ณต์œ ํ•˜๋Š”๋ฐ ์žˆ์–ด ์•ˆ์ „ํ•ด์•ผ ํ•œ๋‹ค๋Š” ๋ง์ด๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, Value type์œผ๋กœ ๋งŒ๋“ค์–ด์ง„ ๊ตฌ์กฐ์ฒด๋“ค(Int, String), ๋˜๋Š” ์• ์ดˆ์— multi thread ํ™˜๊ฒฝ์„ ๊ฐ์•ˆํ•˜๊ณ  ์„ค๊ณ„ํ•œ ๋…€์„๋“ค(Actor, ์ œ๋Œ€๋กœ ์„ค๊ณ„ํ•œ class)์ด ์žˆ๊ฒ ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ์œ„์˜ ์ฝ”๋“œ๋ฅผ ์–ด๋–ป๊ฒŒ ๋ณ€๊ฒฝํ•ด์•ผ ํ• ๊นŒ?

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    try await withThrowingTaskGroup(of: (String, UIImage).self) { group in // โœ…
        for id in ids {
            group.addTask {
                return (id, try await fetchOneThumbnail(withID: id)) // โœ…
            }
        }
        for try await (id, thumbnail) in group { ๐Ÿ…พ๏ธ
            thumbnails[id] = thumbnail
        }
    }
    return thumbnails
}

์ด๋ ‡๊ฒŒ ๋ณ€๊ฒฝํ•ด์ฃผ๋ฉด ๋œ๋‹ค. task์—์„œ๋Š” ๊ฐ’๋งŒ ๋งŒ๋“ค์–ด์„œ returnํ•˜๊ณ , ๊ทธ ๋ชจ๋“  ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์•„์„œ ์ตœ์ข…์ ์œผ๋กœ thumbnails์— ๋ฐ˜์˜ํ•ด์ฃผ๋ฉด ๋œ๋‹ค. ์•ž์ „ ๊ธ€์—์„œ TaskGroup์€ AsyncSequence๋ฅผ ์ฑ„ํƒํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ”๋กœ for (try) await ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

TaskGroup์€ ์ง€๊ธˆ๊นŒ์ง€ ์„ค๋ช…ํ•œ structured concurrency๋ฅผ ๋Œ€๋ถ€๋ถ„ ๋”ฐ๋ฅด์ง€๋งŒ, ๊ตฌํ˜„์— ์žˆ์–ด async let๊ณผ ์•ฝ๊ฐ„์˜ ์ฐจ์ด์ ์ด ์žˆ๋‹ค. ๐Ÿ…พ๏ธ ๋ถ€๋ถ„์„ ๋ณด์ž. TaskGroup์— ๋“ค์–ด๊ฐ„ Child Task๊ฐ€ ๋Œ€๋ถ€๋ถ„ ์ž˜ ๋™์ž‘ํ–ˆ์ง€๋งŒ, ํŠน์ • ๋ถ€๋ถ„์—์„œ Error๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค. ์ด๋Ÿฐ ๊ฒฝ์šฐ, TaskGroup์— ๋“ค์–ด๊ฐ„ ๋ชจ๋“  Task๋Š” ์•”๋ฌต์ ์œผ๋กœ cancel๋˜๊ณ , ๊ฒฐ๊ณผ๋ฅผ awaitํ•œ๋‹ค. ์—ฌ๊ธฐ๊นŒ์ง€๋Š” async let์—์„œ throw๋ฅผ ๋˜์งˆ ๋•Œ, ๊ฐ™์€ depth์— ์žˆ๋Š” ๋‹ค๋ฅธ Task๋ฅผ Cancelํ•˜๋Š” ๊ฒƒ๊ณผ ์œ ์‚ฌํ•˜๋‹ค.

์ฐจ์ด์ ์€, ์ด Cancel์ด ์ƒ์œ„๋กœ ์ „ํŒŒ๋˜๋Š”์ง€ ์—ฌ๋ถ€์— ์žˆ๋‹ค. ์ƒ์œ„ Task์˜ cancellation์€ ๋ฌด์กฐ๊ฑด์ ์ด์ง€ ์•Š๋‹ค. ์ด๋Ÿฐ ๋ฐฉ์‹์€ TaskGroup์„ ์‚ฌ์šฉํ•ด์„œ fork-join ๋ฐฉ์‹์„ ํ‘œํ˜„ํ•˜๋Š” ๊ฒƒ์„ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ค€๋‹ค. ๋˜ ์ˆ˜๋™์ ์œผ๋กœ group์•ˆ์— ๋“ค์–ด๊ฐ„ ๋ชจ๋“  task์˜ ์ž‘์—…์„ cancelAll() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

fork-join pattern: ์–ด๋–ค ๊ณ„์‚ฐ ์ž‘์—…์„ ํ•  ๋•Œ โ€œ์—ฌ๋Ÿฌ ๊ฐœ๋กœ ๋‚˜๋ˆ„์–ด ๊ณ„์‚ฐํ•œ ํ›„ ๊ฒฐ๊ณผ๋ฅผ ๋ชจ์œผ๋Š” ์ž‘์—…โ€

Unstructured Tasks

async let, TaskGroup์€ structured concurrency์— ๋Œ€ํ•œ ์„ค๋ช…์ด์—ˆ๋‹ค. ํ•˜์ง€๋งŒ ํ”„๋กœ๊ทธ๋žจ์„ ์งœ๋‹ค๋ณด๋ฉด, ์ด๋ ‡๊ฒŒ ๊ตฌ์กฐํ™”๋œ ๋ฐฉ๋ฒ•์œผ๋กœ๋งŒ task๋ฅผ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๋„ ์žˆ๋‹ค. ์ด๋Ÿฐ ๋ถ€๋ถ„์—์„œ Swift๋Š” ์œ ์—ฐ์„ฑ์„ ์ œ๊ณตํ•œ๋‹ค.

Parent Task๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ๋„ ์žˆ๋‹ค. ๊ทธ์ € ๋™๊ธฐ ์ฝ”๋“œ์—์„œ ๋น„๋™๊ธฐ ์ฝ”๋“œ๋ฅผ ํ•œ๋ฒˆ ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์„ ์ˆ˜ ์žˆ๋‹ค.

ํ˜น์€ ํŠน์ • Task์˜ lifecycle์ด ํŠน์ • ๋ฒ”์œ„๋ฅผ ๋„˜์–ด์„œ ์กด์žฌํ•˜๊ณ  ์‹ถ์„ ์ˆ˜๋„ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, Object๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋Š” A method์˜ ์‘๋‹ต์„ ๊ธฐ๋ฐ˜์œผ๋กœ Task๋ฅผ ์‹คํ–‰์‹œํ‚ค๊ณ , Object๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜๋Š” B method์˜ ์‘๋‹ต์œผ๋กœ Task๋ฅผ cancelํ•˜๊ณ  ์‹ถ์„ ์ˆ˜ ์žˆ๋‹ค.

@MainActor
class MyDelegate: UICollectionViewDelegate {
    func collectionView(_ view: UICollectionView,
                        willDisplay cell: UICollectionViewCell,
                        forItemAt item: IndexPath) {
        let ids = getThumbnailIDs(for: item)
        let thumbnails = await fetchThumbnails(for: ids) // โŽ 'await' in a function that does not support concurrency
        display(thumbnails, in: cell)
    }
}

์ด๋Ÿฐ ๊ฒฝ์šฐ๋Š” AppKit๊ณผ UIKit์—์„œ delegate object๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์ž์ฃผ ๋ฐœ์ƒํ•œ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, collectionView๊ฐ€ ์žˆ๊ณ , ์•„์ง collectionView์˜ dataSource api๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ๋ชปํ•œ๋‹ค๊ณ  ํ•˜์ž. ์ด ์ƒํ™ฉ์—์„œ ์ผ๋‹จ์€ thumbnail์„ ๋„คํŠธ์›Œํฌ์—์„œ ์š”์ฒญํ•˜๋ ค๊ณ  ์œ„์™€ ๊ฐ™์ด ์ ์—ˆ๋‹ค. ํ•˜์ง€๋งŒ, collectionViewDelegate method๋Š” asyncํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์œ„์ฒ˜๋Ÿผ compile error๊ฐ€ ๋‚œ๋‹ค.

@MainActor
class MyDelegate: UICollectionViewDelegate {
    func collectionView(_ view: UICollectionView,
                        willDisplay cell: UICollectionViewCell,
                        forItemAt item: IndexPath) {
        let ids = getThumbnailIDs(for: item)
        Task {
            let thumbnails = await fetchThumbnails(for: ids)
            display(thumbnails, in: cell)    
        }
    }
}

์ด๋Ÿฐ ๊ฒฝ์šฐ Task๋กœ ๊ฐ์‹ธ์„œ collectionView์˜ scope๋ฅผ ๋ฒ—์–ด๋‚˜, DispatchQueue.main.async์™€ ๊ฐ™์€ ๋™์ž‘์„ ํ•˜๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด collectionView์˜ scope ๋ฐ–์—์„œ main thread์— ํ•ด๋‹น ์ž‘์—…์ด ๋“ค์–ด๊ฐ€๊ณ , ์ˆ˜ํ–‰๋œ๋‹ค. ์ด๋ ‡๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ์˜ ์žฅ์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. ์ƒ์œ„ ์ž‘์—…์˜ actor isolation๊ณผ priority๋ฅผ ์ƒ์†๋ฐ›๋Š”๋‹ค.
  2. Lifetime์ด ์–ด๋– ํ•œ scope์—๋„ ๊ตญํ•œ๋˜์ง€ ์•Š๋Š”๋‹ค.
  3. ์–ด๋””์„œ๋“  ์‹คํ–‰๊ฐ€๋Šฅํ•˜๋‹ค. ์‹ฌ์ง€์–ด asyncํ•˜์ง€ ์•Š์€ ํ•จ์ˆ˜์—์„œ๋„
  4. ์ˆ˜๋™์œผ๋กœ cancelํ•˜๊ณ  awaitํ•  ์ˆ˜ ์žˆ๋‹ค.
@MainActor
class MyDelegate: UICollectionViewDelegate {
    var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:]
 
    func collectionView(_ view: UICollectionView,
                        willDisplay cell: UICollectionViewCell,
                        forItemAt item: IndexPath) {
        let ids = getThumbnailIDs(for: item)
        thumbnailTasks[item] = Task {
            defer { thumbnailTasks[item] = nil } // ์ผ๋‹จ ํ™”๋ฉด์— ๋ณด์—ฌ์คฌ์œผ๋ฉด Task๋Š” ํ•„์š”์—†์Œ. cancelํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ๊ฒƒ
            let thumbnails = await fetchThumbnails(for: ids)
            display(thumbnails, in: cell)    
        }
    }
}

์˜ˆ๋ฅผ ๋“ค์–ด, scroll ๋œ ๊ฒฝ์šฐ, ํ•ด๋‹น Task๋ฅผ cancelํ•ด๋ฒ„๋ฆด ์ˆ˜ ์žˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•  ๊ฒฝ์šฐ, data race ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•  ์ง€๋„ ๋ชจ๋ฅด๊ฒ ๋‹ค. ํ•˜์ง€๋งŒ delegate class๋Š” ํ˜„์žฌ main actor์ด๊ณ , ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ๋งŒ๋“  Task๋Š” ๊ทธ ํŠน์ง•์„ ๋ชจ๋‘ ์ƒ์†๋ฐ›๋Š”๋‹ค. ๋”ฐ๋ผ์„œ main thread์—์„œ syuc ํ•˜๊ฒŒ ๋™์ž‘ํ•˜์—ฌ ๋ณ‘๋ ฌ์ ์œผ๋กœ ๋™์ž‘ํ•  ์ˆ˜ ์—†๋‹ค. main actor๋กœ ์„ ์–ธ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅ ํ”„๋กœํผํ‹ฐ์— ์ ‘๊ทผํ•˜์—ฌ ๊ฐ’์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.

@MainActor
class MyDelegate: UICollectionViewDelegate {
    var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:]
 
    func collectionView(_ view: UICollectionView,
                        willDisplay cell: UICollectionViewCell,
                        forItemAt item: IndexPath) {
        let ids = getThumbnailIDs(for: item)
        thumbnailTasks[item] = Task {
            defer { thumbnailTasks[item] = nil } // ์ผ๋‹จ ํ™”๋ฉด์— ๋ณด์—ฌ์คฌ์œผ๋ฉด Task๋Š” ํ•„์š”์—†์Œ. cancelํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ๊ฒƒ
            let thumbnails = await fetchThumbnails(for: ids)
            display(thumbnails, in: cell)
        }
    }
 
    func collectionView(_ view: UICollectionView,
                        didEndDisplay cell: UICollectionViewCell, // collectionView์—์„œ ์ง€์›Œ์ง„ ๊ฒฝ์šฐ
                        forItemAt item: IndexPath) {
        thumbnailTasks[item]?.cancel()
    }
}

collectionView์—์„œ ์ง€์›Œ์กŒ๋Š”๋ฐ, ํ˜„์žฌ thumbnail์„ ๋ฐ›์•„์˜ค๊ณ  ์žˆ๋‹ค๋ฉด, ์ด ๊ฒฝ์šฐ์—๋Š” Task๋ฅผ cancelํ•ด์•ผ ํ•œ๋‹ค.

Detached tasks

๋ณด๋‹ค ๊ฐ•ํ•œ ์œ ์—ฐ์„ฑ์„ ์œ„ํ•ด ๋งŒ๋“ค์–ด์ง„ task์ด๋‹ค.

  • unstructured task์ด๋‹ค.
    • ์ฆ‰, lifetime์ด scoping๋˜์ง€ ์•Š๋Š”๋‹ค.
    • ์ˆ˜๋™์ ์œผ๋กœ cancel, await ๊ฐ€๋Šฅํ•˜๋‹ค.
  • ํ•˜์ง€๋งŒ, ํ•ด๋‹น Task๊ฐ€ ์œ„์น˜ํ•œ context์—์„œ ์–ด๋– ํ•œ ๊ฒƒ๋„ ์ƒ์†๋ฐ›์ง€ ์•Š๋Š”๋‹ค.
    • ์ฆ‰, ๋…๋ฆฝ์ ์œผ๋กœ ์ž‘๋™ํ•œ๋‹ค.
    • priority์™€ traits๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค.

thumbnail์„ server์—์„œ ๋ฐ›์•„์˜ค๊ณ , ์ด๋ฅผ local disk cache์— ์ €์žฅํ•˜๋Š” ์˜ˆ์‹œ๋ฅผ ๋“ค์–ด๋ณด์ž. caching ์ž‘์—…์€ main thread์—์„œ ์ผ์–ด๋‚  ํ•„์š”๊ฐ€ ์—†๋‹ค.

@MainActor
class MyDelegate: UICollectionViewDelegate {
    var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:]
 
    func collectionView(_ view: UICollectionView,
                        willDisplay cell: UICollectionViewCell,
                        forItemAt item: IndexPath) {
        let ids = getThumbnailIDs(for: item)
        thumbnailTasks[item] = Task {
            defer { thumbnailTasks[item] = nil } 
            let thumbnails = await fetchThumbnails(for: ids)
 
            Task.detached(priority: .background) { // โœ…
                writeToLocalCache(thumbnails)
            }
 
            display(thumbnails, in: cell)
        }
    }
}

์ €์žฅ์˜ ๊ฒฝ์šฐ, ์šฐ์„ ์ˆœ์œ„๋„ ๋‚ฎ๊ณ , main thread์—์„œ ๋™์ž‘ํ•  ํ•„์š”๊ฐ€ ์—†์œผ๋‹ˆ, ์ด๋Ÿฐ ๊ฒฝ์šฐ detached ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์œ ์šฉํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ, ๋งŒ์•ฝ background๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” ๋‹ค์–‘ํ•œ task๊ฐ€ ์žˆ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•ด์•ผ ํ• ๊นŒ? ์ผ์ผํžˆ Task.detached ๋ฅผ ํ†ตํ•ด์„œ ์ฒ˜๋ฆฌํ•ด์ฃผ์–ด์•ผ ํ• ๊นŒ?

@MainActor
class MyDelegate: UICollectionViewDelegate {
    var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:]
 
    func collectionView(_ view: UICollectionView,
                        willDisplay cell: UICollectionViewCell,
                        forItemAt item: IndexPath) {
        let ids = getThumbnailIDs(for: item)
        thumbnailTasks[item] = Task {
            defer { thumbnailTasks[item] = nil } 
            let thumbnails = await fetchThumbnails(for: ids)
 
            Task.detached(priority: .background) {
                withTaskGroup(of: Void.self) { group in
                    group.async { writeToLocalCache(thumbnails) }
                    group.async { log(thumbnails) }
                    group.async { ... }
                }
            }
 
            display(thumbnails, in: cell)
        }
    }
}

์ด์™€ ๊ฐ™์ด TaskGroup์„ ์‚ฌ์šฉํ•˜๋ฉด ์ข‹๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด, ์ผ์ผํžˆ Task๋ฅผ ๋งŒ๋“ค์–ด ๊ด€๋ฆฌํ–ˆ์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ํ•˜๋‚˜์”ฉ cancelํ•ด์•ผํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค. ๋‚ด๋ถ€์ ์œผ๋กœ group์œผ๋กœ ๋ฌถ์—ฌ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ƒ์œ„์—์„œ cancelํ•˜๋ฉด ์ „ํŒŒ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋˜ ์ƒ์œ„์˜ ํŠน์„ฑ์„ ๋ชจ๋‘ inheritํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ค‘๋ณต๋˜๋Š” ์ฝ”๋“œ๋„ ์ ์–ด์ง„๋‹ค.

Flavors of tasks

์ง€๊ธˆ๊นŒ์ง€ ๋ชจ๋‘ ์•Œ์•„๋ณธ ๊ฒƒ์„ ์ •๋ฆฌํ•ด๋ณด์ž. ์ด๊ฑธ ๋ณด๊ณ  ๋จธ๋ฆฌ์— ๋“ค์–ด์™”๋‹ค๋ฉด ๋‹ค ์ดํ•ดํ•œ ๊ฒƒ์ด๋‹ค.

Launched byLaunchable fromLifetimeCancellationInherits from origin
async-let tasksasync-let ~async functionsscoped to statementautomaticpriority
task-local values
Group tasksgroup.asyncwithTaskGroupscoped to task groupautomaticpriority
task-local values
Unstructured tasksTaskanywhereunscopedvia Taskpriority
task-local values
actor
Detached tasksTask.detachedanywhereunscopedvia Tasknothing

์„ฑ๋Šฅ์— ๋Œ€ํ•œ ์ข‹์€ ๊ธ€์ด ์žˆ์–ด ์ฒจ๋ถ€ํ•œ๋‹ค. Swift Concurrency์— ๋Œ€ํ•ด์„œ

Reference