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 ํ๊ฐ ๋ฐฉ์์ ๋ํด ์ดํดํด๋ณด์.
- ์ด์ ์ํ์์ Child Task๋ฅผ ๋ง๋ ๋ค.
- Child Task์์์
async let
์ผ๋ก async ํจ์๋ฅผ ํธ์ถํ๋ค. - Parent Task(์ด์ ์ํ์์ ์ฌ์ฉํ๋ Task)๋ฅผ ์ํด
result
์ placeholder๋ฅผ ํ ๋นํ๋ค. - ์ค์ ๋์(
URLSession.shared.data()
)์ Child Task์์ ์ํํ๋ค. - Parent Task๋ ๋คํธ์ํฌ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ ์งํํ๋ค.
- ํ์ง๋ง ์ค์ ๋ก Parent Task๊ฐ ๋ค์ด๋ก๋๋ ๊ฐ์ ํ์๋ก ํ๋ค๋ฉด,
await
๋ฅผ ํตํด child Task์ ๋์์ ๋๊ธฐํ ์ ์๋ค. - ๋ง์ฝ 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์ ํด๋น ์์
์ด ๋ค์ด๊ฐ๊ณ , ์ํ๋๋ค. ์ด๋ ๊ฒ ์ฒ๋ฆฌํ๋ ๊ฒฝ์ฐ์ ์ฅ์ ์ ๋ค์๊ณผ ๊ฐ๋ค.
- ์์ ์์ ์ actor isolation๊ณผ priority๋ฅผ ์์๋ฐ๋๋ค.
- Lifetime์ด ์ด๋ ํ scope์๋ ๊ตญํ๋์ง ์๋๋ค.
- ์ด๋์๋ ์คํ๊ฐ๋ฅํ๋ค. ์ฌ์ง์ด asyncํ์ง ์์ ํจ์์์๋
- ์๋์ผ๋ก 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 by | Launchable from | Lifetime | Cancellation | Inherits from origin | |
---|---|---|---|---|---|
async-let tasks | async-let ~ | async functions | scoped to statement | automatic | priority task-local values |
Group tasks | group.async | withTaskGroup | scoped to task group | automatic | priority task-local values |
Unstructured tasks | Task | anywhere | unscoped | via Task | priority task-local values actor |
Detached tasks | Task.detached | anywhere | unscoped | via Task | nothing |
์ฑ๋ฅ์ ๋ํ ์ข์ ๊ธ์ด ์์ด ์ฒจ๋ถํ๋ค. Swift Concurrency์ ๋ํด์