본문 바로가기

iOS+/Swift

Swift Concurrency

개요

Swift Concurrency는 Apple이 Swift 5.5에서 도입한 현대적인 동시성 모델로, 동시성 코드를 작성하는 데 있어 더욱 간결하고 안전하며 가독성을 높이는 방식을 제공한다

이를 통해 개발자는 복잡한 비동기 작업을 보다 쉽게 관리하고, 동시성 문제(예: 데이터 경쟁 및 데드락)를 줄일 수 있다

 

이전까지는 GCD(Grand Central Dispatch)를 이용하여 애플 플랫폼에서 동시성 프로그래밍을 진행했으나 여러 단점들이 있었는데, Swift Conccurency를 통해 이러한 문제들을 해결하였다

 

이 글에서는 우선 GCD 방식에 대해 간략하게 알아보고, Swift Concurrecny로 동시성 프로그래밍 코드를 작성하는 방법에 대해 정리한다

 

  • GCD(이전 방식)
  • Swift Concurrecny
  • Task와 TaskGroup
  • AsyncStream
  • Actor

 

GCD(Grand Central Dispatch)

https://medium.com/swift2go/beginning-swift-programming-part-11-grand-central-dispatch-and-closures-293132b6a69d

 

GCD는 OS 수준에서 병렬 처리 및 동시성 프로그래밍을 지원하는 기술이다

직접 스레드를 관리할 필요 없이, DispatchQueue를 통해 멀티 코어와 멀티 프로세싱 환경에서 최적화된 프로그래밍을 할 수 있도록 애플이 개발했다

즉, GCD에서는 작업을 큐에 보내면, 그에 따른 스레드를 적절히 생성해서 분배한다

좀 더 엄밀히 말하면 callback인 Closure를 Worker Pools에서 실행하도록 해주는 Queue다

DispatchQueue 종류

Main Queue - Serial

주로 UI 업데이트나 사용자 이벤트 처리를 담당

 

DispatchQueue.main.async {
    // UI 업데이트는 항상 메인 큐에서 실행
    self.label.text = "Hello, Main Queue!"
}

Global Queue - Concurrent

백그라운드 작업(예: 데이터 처리, 네트워크 요청 등)에 사용

 

DispatchQueue.global(qos: .background).async {
    print("Background task running...")
}

Private Queue - Serial / Concurrent

개발자가 직접 생성하여 특정 작업을 분리하거나 제어할 때 사용

 

let serialQueue = DispatchQueue(label: "serial")
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)

예시) I/O 작업에서 Sync vs Async

I/O 작업은 파일 읽기/쓰기, 네트워크 요청과 같은 작업을 포함하며, Sync와 Async 방식의 차이를 다음 예시에서 확인할 수 있다

Sync(동기 처리)

작업이 완료될 때까지 호출한 스레드가 차단되고, 작업 완료 후에만 다음 코드가 실행된다

 

let queue = DispatchQueue(label: "ioQueue")
var fileData: Data?

queue.sync {
    do {
        fileData = try Data(contentsOf: URL(fileURLWithPath: "/path/to/file"))
    } catch {
        print("Failed to read file: \\(error)")
    }
}

if let data = fileData {
    print("File data loaded")
}

// 이 부분은 파일 읽기 작업 완료 후 실행된다
print("Finish")

Async (비동기 처리)

작업이 완료되기를 기다리지 않고 다음 코드가 바로 실행되고, 작업 완료 후 콜백에서 결과를 처리한다

 

let queue = DispatchQueue(label: "ioQueue")

queue.async {
    do {
        let fileData = try Data(contentsOf: URL(fileURLWithPath: "/path/to/file"))
        print("File data loaded")
    } catch {
        print("Failed to read file: \\(error)")
    }
}

// 이 부분은 파일 읽기 작업이 끝나기 전에 실행된다
print("Finish")

 

Swift Concurrency

 

https://sungjk.github.io/2021/08/01/what-is-coroutine.html

 

코루틴(Coroutine)은 함수가 동작하는 도중 특정 시점에 일시정지(suspend)하고 다시 재개(resume)할 수 있다

즉, 일반적으로 사용하던 Main-SubRoutine(좌측 이미지)과 달리 제어권이 여러 번 중간중간 왔다갔다 할 수 있다

 

Swift Concurrency를 이용하면, 이런 방식을 기반으로 비동기 코드 작성 시 이용할 수 있다

Swift는 코어 수 만큼의 협력 스레드협력 스레드풀에 미리 만들고, 이곳에 Task가 배치된다

 

이 Task가 suspend 되어도 협력 스레드의 스택 영역Continuation으로 문맥이 저장되기 때문에, 컨텍스트 스위칭 없이 resume 가능하다

 

이런 방식은 이전에 사용하던 GCD에 비해 다음과 같은 장점을 갖는다

 

  • Thread Pool로 관리하기에 Thread Explosion이 발생하지 않는다
  • Context Switching 비용이 필요 없다
  • 비동기 함수를 동기적 코드로 작성할 수 있어 가독성이 향상된다(async/await)
  • async/await 키워드를 사용하면 기존 콜백에서 이용되던 self 키워드에 대한 접근이 필요 없어져서 retain cycle이 발생할 우려가 사라졌다

구조화된 동시성과 구조화되지 않은 동시성

  • 구조화된 동시성은 비동기 작업들이 특정 컨텍스트 내에서 완료되는 것을 의미한다
  • 구조화되지 않은 동시성은 비동기 작업들이 수행되지만 그 결과들이 범위 밖에서 처리되는 것을 의미한다

CPU 작업과 I/O 작업의 이해

Swift Concurrency를 잘 이용하려면 CPU 작업과 I/O 작업을 이해해야 한다

Task는 협력 스레드에서 수행되는데, CPU 작업이라면, 해당 작업이 완료될 때까지 해당 협력스레드를 점유하게 된다

그래서 CPU 작업과 I/O 작업이 있을 때, Swift Concurrency를 이용하려면 Task의 priority를 잘 지정해야 한다

 

이 말이 무슨 의미인지는 다음 두 예시를 통해 이해할 수 있다

우선 아래 예시에서 사용되는 sleep과 Task.sleep 메서드는 다음과 같은 차이가 존재한다

 

  • sleep() - 호출한 스레드를 블로킹, 현재 스레드가 지정된 시간 동안 멈추고 다른 작업을 수행하지 못함
  • Task.sleep(nanoseconds:) - 비동기 환경에서만 사용 가능하며 호출한 스레드를 블로킹하지 않음

아래 예시에서는 CPU 작업을 흉내내기 위해 sleep 메서드를 사용하고, IO 작업을 흉내내기 위해 Task.sleep 메서드를 사용했다

 

예시1 ) 협력 스레드 점유

더보기

예시1 ) 협력 스레드 점유

5초가 걸리는 CPU 작업을 협력 스레드 수 보다 2배 많이 Swift Concurrency로 수행시키면, 협력 스레드는 작업을 마칠 때까지 점유되느라 총 10초가 걸린다

 

import Foundation

// 5초가 걸리는 CPU 작업
func asyncWork() async {
    print("\\(Thread.current) - Work started")
    sleep(5)
    print("\\(Thread.current) - Work finished")
}

let cores = ProcessInfo.processInfo.activeProcessorCount  // 코어 수를 가져옵니다
print("Number of cores: \\(cores)")

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
print("Current time before tasks: \\(dateFormatter.string(from: Date()))")

// 코어 수*2 만큼 비동기 작업을 생성하여 수행시킵니다
Task {
    await withTaskGroup(of: Void.self) { group in
        for _ in 0..<cores*2 {
            group.addTask {
                await asyncWork()
            }
        }
    }
    print("Current time after tasks: \\(dateFormatter.string(from: Date()))")
}

 

 

Number of cores: 8
Current time before tasks: 2024-03-30 11:48:14
<NSThread: 0x60000170ce80>{number = 5, name = (null)} - Work started
<NSThread: 0x600001723800>{number = 8, name = (null)} - Work started
<NSThread: 0x60000170e9c0>{number = 3, name = (null)} - Work started
<NSThread: 0x6000017094c0>{number = 10, name = (null)} - Work started
<NSThread: 0x6000017100c0>{number = 9, name = (null)} - Work started
<NSThread: 0x600001709900>{number = 12, name = (null)} - Work started
<NSThread: 0x600001710100>{number = 11, name = (null)} - Work started
<NSThread: 0x60000171c080>{number = 6, name = (null)} - Work started
<NSThread: 0x60000170ce80>{number = 5, name = (null)} - Work finished
<NSThread: 0x60000170ce80>{number = 5, name = (null)} - Work started
<NSThread: 0x60000170e9c0>{number = 3, name = (null)} - Work finished
<NSThread: 0x6000017094c0>{number = 10, name = (null)} - Work finished
<NSThread: 0x6000017100c0>{number = 9, name = (null)} - Work finished
<NSThread: 0x600001709900>{number = 12, name = (null)} - Work finished
<NSThread: 0x6000017094c0>{number = 10, name = (null)} - Work started
<NSThread: 0x600001709900>{number = 12, name = (null)} - Work started
<NSThread: 0x600001723800>{number = 8, name = (null)} - Work finished
<NSThread: 0x60000170e9c0>{number = 3, name = (null)} - Work started
<NSThread: 0x600001723800>{number = 8, name = (null)} - Work started
<NSThread: 0x6000017100c0>{number = 9, name = (null)} - Work started
<NSThread: 0x600001710100>{number = 11, name = (null)} - Work finished
<NSThread: 0x60000171c080>{number = 6, name = (null)} - Work finished
<NSThread: 0x600001710100>{number = 11, name = (null)} - Work started
<NSThread: 0x60000171c080>{number = 6, name = (null)} - Work started
<NSThread: 0x6000017100c0>{number = 9, name = (null)} - Work finished
<NSThread: 0x60000170e9c0>{number = 3, name = (null)} - Work finished
<NSThread: 0x600001710100>{number = 11, name = (null)} - Work finished
<NSThread: 0x60000170ce80>{number = 5, name = (null)} - Work finished
<NSThread: 0x60000171c080>{number = 6, name = (null)} - Work finished
<NSThread: 0x600001709900>{number = 12, name = (null)} - Work finished
<NSThread: 0x6000017094c0>{number = 10, name = (null)} - Work finished
<NSThread: 0x600001723800>{number = 8, name = (null)} - Work finished
Current time after tasks: 2024-03-30 11:48:24

 

8개의 코어에 5초간 수행되어야 하는 작업들이 전부 코어에서 수행 중일 때는 다른 작업이 할당 안되고,

완료가 되고 나서야 남은 작업들이 배치될 수 있게 되어서 10초가 걸리게 되는 것을 알 수 있다

 

즉, CPU 작업을 Swift Concurrecy로 수행하면, 작업을 하는 동안 CPU가 점유되어 다른 작업을 할 수 없다

그렇지만 I/O 작업의 경우 이야기가 달라진다

 

I/O 작업들은 CPU가 아닌 곳에서 수행되기에 작업을 수행시키고, 완료되면 결과만 얻어오면 된다

 

즉, 작업을 하는 동안은 CPU가 점유될 필요가 없다

그리고 작업을 시키고 다른 작업을 수행하는 협력 스레드는 Continuation을 통해 문맥을 관리하기에 컨텍스트 스위칭도 발생하지 않게 된다

그래서 이전 예시에선 작업이 CPU를 점유해서 지연되었던 것과 달리, 다음 예시는 작업이 몇 개든 간에 5초 안에 끝낼 수 있다

 

import Foundation

// 비동기 작업 함수 정의
func asyncWork() async {
    print("\\(Thread.current) - Work started")
    await Task.sleep(5_000_000_000)  // Task로 할때는 nano초 단위로 생성
    print("\\(Thread.current) - Work finished")
}

let cores = ProcessInfo.processInfo.activeProcessorCount  // 코어 수를 가져옵니다
print("Number of cores: \\(cores)")

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
print("Current time before tasks: \\(dateFormatter.string(from: Date()))")

// 코어 수*10 만큼 비동기 작업을 생성하여 수행시킵니다
Task {
    await withTaskGroup(of: Void.self) { group in
        for _ in 0..<cores*10 {
            group.addTask {
                await asyncWork()
            }
        }
    }
    print("Current time after tasks: \\(dateFormatter.string(from: Date()))")
}

Number of cores: 8
Current time before tasks: 2024-03-30 11:56:21
<NSThread: 0x60000170c780>{number = 5, name = (null)} - Work started
<NSThread: 0x60000170c780>{number = 5, name = (null)} - Work started
<NSThread: 0x60000170c780>{number = 5, name = (null)} - Work started
<NSThread: 0x600001706580>{number = 3, name = (null)} - Work started
<NSThread: 0x600001706580>{number = 3, name = (null)} - Work started
<NSThread: 0x600001705dc0>{number = 6, name = (null)} - Work started
<NSThread: 0x6000017180c0>{number = 9, name = (null)} - Work started
...
Current time after tasks: 2024-03-30 11:56:26

 

한 스레드에서 연속적으로 수행될 수 있는 이유는 asyncWork() 내부에서 await을 통해 suspend 되었기 때문이다 

예시2) Swift Concurrency의 스케쥴링

더보기

예시2) Swift Concurrency의 스케쥴링

이전 예시에서 CPU작업과 I/O작업을 동시에 수행해야 하는 경우는 어떨까?

 

만약 5초가 걸리는 CPU 작업과 5초가 걸리는 I/O 작업이 있을 때, 스케쥴링을 이용하면 각 작업들을 적절하게 수행시켜 CPU 작업들이 협력 스레드를 점유해 버리는 상황을 해결할 수 있다

 

I/O 작업은 호출 후, await을 기다리면 되기에 priority를 .high로 지정한다

반면에 CPU 작업은 I/O 작업이 우선적으로 호출되고 수행되도록 priority를 .low로 지정한다

 

이처럼 Task에 priority를 지정하여 최대한 효율적인 스케쥴링이 적용되도록 할 수 있다

 

import Foundation

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"

// 5초가 걸리는 CPU 작업
func asyncWork() async {
    print("\\(dateFormatter.string(from: Date()))\\(Thread.current) - Work started")
    sleep(5)
    print("\\(dateFormatter.string(from: Date()))\\(Thread.current) - Work finished")
}

// 5초가 걸리는 I/O 작업
func asyncIOWork() async {
    print("\\(dateFormatter.string(from: Date()))\\(Thread.current) - asyncIO Work started")
    try? await Task.sleep(nanoseconds: 5_000_000_000)
    print("\\(dateFormatter.string(from: Date()))\\(Thread.current) - asyncIO Work finished")
}

let cores = ProcessInfo.processInfo.activeProcessorCount  // 코어 수를 가져옵니다
print("Number of cores: \\(cores)")
print("Current time before tasks: \\(dateFormatter.string(from: Date()))")

// 코어 수*2 만큼 비동기 작업을 생성하여 수행시킵니다
Task(priority: .low) {
    await withTaskGroup(of: Void.self) { group in
        for _ in 0..<cores*2 {
            group.addTask {
                await asyncWork()
            }
        }
    }
    
    print("Current time after asyncWork: \\(dateFormatter.string(from: Date()))")
}
 
// 코어 수*20 만큼 비동기 작업을 생성하여 수행시킵니다 
Task(priority: .high) {
    await withTaskGroup(of: Void.self) { group in
        for _ in 0..<cores*20 {
            group.addTask {
                await asyncIOWork()
            }
        }
    }
    
    print("Current time after asyncIOWorks: \\(dateFormatter.string(from: Date()))")
}

 

Task

Swift Concurrency의 구조화된 동시성은 작업의 수명 주기를 명확히 관리하여 예상치 못한 동작을 줄이는 데 도움을 준다

Task는 Swift의 동시성 시스템에서 비동기 작업을 나타내는 단위다

 

전체적인 타입은 Task<Success, Failure>로 제네릭 타입이며, Success(반환 값)Failure(에러) 타입을 명시한다

참고로 .cancel()로 진행 중인 Task를 취소시킬 수 있다

Task<Success, Failure>

반환값이 없을 때

Task(Success = Void, Failure = Never)로 생각할 수 있다

 

let simpleTask = Task {
    await doSomething()
}

반환값이 있을 때

Task에 .value를 통해 값을 얻을 수 있다

 

let stringTask = Task<String, Never> {
    return "Hello, World!"
}
let result = await stringTask.value  // "Hello, World!"

에러가 발생할 때

Task 결과가 아닌 에러가 발생했을 수도 있기에, 이를 처리하는 과정이 필요하다

이때 .result.get()를 통해 결과를 언래핑할 수 있다

 

enum FetchError: Error {
    case networkError
}

let fetchTask = Task<Data, Error> {
    guard let data = try await fetchData() else {
        throw FetchError.networkError
    }
    return data
}

do {
    let data = try await fetchTask.result.get()
// data 처리
} catch {
// 에러 처리
}

 

또는 다음과 같이 .result만 이용해서 switch-case문으로 처리할 수도 있다

 

let result = await fetchTask.result
switch result {
case .success(let data):
// data 처리
case .failure(let error):
// 에러 처리
}

Task.detached

Task.detached는 비동기 작업을 현재의 비동기 컨텍스트에서 분리된 새로운 태스크(작업)로 실행하도록 하는 메서드다

그렇기 때문에, 부모 Task가 취소되어도 영향을 받지 않는다

 

Task.detached 사용이 적합한 경우

  • 백그라운드 로깅/분석
  • 캐시 업데이트
  • 독립적인 네트워크 요청
  • 부모 태스크의 취소와 무관하게 실행되어야 하는 작업

이러한 경우들에서 Task.detached를 사용하면 메인 로직과 독립적으로 작업을 수행할 수 있다

 

func startLongRunningTask() async {
    // 메인 작업
    await doSomeWork()

    // 이 작업은 메인 작업과 독립적으로 실행됩니다.
    Task.detached {
        await doSomeIndependentWork()
    }
}

 

위 예시코드에서 doSomeIndependentWork()는 startLongRunningTask()의 실행과는 독립적으로 동작한다

그래서 startLongRunningTask()가 종료되더라도 doSomeIndependentWork()는 그대로 진행된다

 

TaskGroup

TaskGroup은 여러 비동기 작업을 동시에 실행하고 관리하기 위한 Swift의 구조화된 동시성 도구다

단일 작업을 여러 하위 작업으로 분할하여 병렬로 실행할 때 사용한다

 

withTaskGroupwithThrowingTaskGroup 함수를 이용하여 태스크들을 그룹으로 관리한다

@backDeployed(before: macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0)
func withTaskGroup<ChildTaskResult, GroupResult>(
    of childTaskResultType: ChildTaskResult.Type,
    returning returnType: GroupResult.Type = GroupResult.self,
    isolation: isolated (any Actor)? = #isolation,
    body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
) async -> GroupResult where ChildTaskResult : Sendable
  • of childTaskResultType : 이 파라미터는 하위 작업의 결과 타입을 지정
  • returning returnType : 이 파라미터는 TaskGroup 전체에서 반환할 결과 타입을 지정
  • isolation : 비동기 작업이 실행되는 컨텍스트를 지정, 기본값은 nil
  • body : 이 클로저는 inout TaskGroup<ChildTaskResult>를 받아, 비동기 작업들을 그룹 내에서 정의하고 추가하는 역할로, 클로저가 종료되면 TaskGroup 내의 모든 하위 작업이 실행되고, 그 결과를 종합하여 반환

작업 추가

body 클로저의 인자에 group이 전달되는데, group.addTask(_:)를 이용하면, TaskGroup에 Task을 추가할 수 있다

 

await withTaskGroup(of: Int.self) { group in
    for number in 1...5 {
        group.addTask {
            return number * 2
        }
    }
    ...

작업 결과

TaskGroup의 결과는 한 번에 완료된 모든 작업의 결과(waitForAll)를 얻을 수도 있고, 하나하나 완료된 작업의 결과(for await-)를 얻을 수 있다

waitForAll()

TaskGroup 내의 모든 작업이 완료될 때까지, body 클로저의 인자에 전달된 group를 통해 group.waitForAll()을 이용하면 기다릴 수 있다

await withTaskGroup(of: Int.self) { group in
    for number in 1...5 {
        group.addTask {
            return number * 2
        }
    }

    var total = 0

    await group.waitForAll()  // 모든 작업이 완료될 때까지 대기
    for await result in group {
        total += result
    }
    print("Total: \\(total)")
}

for await -

TaskGroup은 Swift의 AsyncSequence 프로토콜을 준수한다

그렇기에 next() 메서드로 개별 결과 접근 가능하며, for await 루프를 이용할 수 있다

하지만 작업 완료 순서는 생성 순서와 다를 수 있다

 

func demonstrateOrder() async {
    let result = await withTaskGroup(of: String.self) { group in
        group.addTask { await slowTask("First") }
        group.addTask { await fastTask("Second") }

        var collected = [String]()
        while let next = await group.next() {
            collected.append(next)
        }
        return collected
    }
    print(result)  // ["First", "Second"] 또는 ["Second", "First"]
}

예시) withTaskGroup, withThrowingTaskGroup

withTaskGroup는 에러를 던지지 않는 일반적인 태스크 그룹을 만들 때 사용된다

 

let sum = await withTaskGroup(of: Int.self) { group in
    for number in 1...5 {
        group.addTask {
            return number * 2
        }
    }

    var total = 0
    for await result in group {
        total += result
    }
    return total
}

 

withThrowingTaskGroup는 에러를 던질 수 있는 태스크 그룹을 만들 때 사용된다

 

func fetchData() async throws -> [Data] {
    try await withThrowingTaskGroup(of: Data.self) { group in
        for url in urls {
            group.addTask {
                try await fetchData(from: url)
            }
        }

        var dataArray = [Data]()
        for try await data in group {
            dataArray.append(data)
        }
        return dataArray
    }
}

 

AsyncStream

AsyncStream을 설명하기에 앞서 AsyncSequence의 사용 예시를 본다

 

func fetchUsers() async throws {
    let url = URL(string: "<https://hws.dev/users.csv>")!

    for try await line in url.lines {
        print("Received user: \\(line)")
    }
}

try? await fetchUsers()

 

AsyncSequence 프로토콜은 비동기적으로 요소를 반환하는 것 말고는 Sequence 프로토콜과 거의 동일하다

Sequene와 달리 AsyncSequence는 for문에서 await과 사용될 수 있다(URL의 lines가 AsyncSequence 프로토콜을 준수한다)

 

또한 map, filter, contains, reduce 같이 기존 Sequence에서 사용했던 메서드들이 존재한다(AsyncMapSequence, AsyncFilterSequence…)

 

그렇다면 AsyncStream은 무엇인가?

 

AsyncStream은 AsyncSequence의 구현체 중 하나며, 비동기적으로 생성된 값의 스트림을 제공하는 방법을 정의한다

 

아래 예시와 같이 continuation을 클로저 인자로 받으면, continuation.yield(_:)를 통해 값을 내보낼 수 있다

그리고 AsyncStream을 사용하는 부분에서 계속 suspend 되는 것을 막기위해, 스트림의 종료를 명시적으로 continuation.finish()를 사용해서 알려야 한다

 

func numbers() -> AsyncStream<Int> {
    AsyncStream<Int> { continuation in
        for i in 1...5 {
            continuation.yield(i)
        }
        continuation.finish()
    }
}

Task {
    for await number in numbers() {
        print(number)
    }
}

 

Actor

actor스레드 안전성을 보장하는 동시성 타입으로, 클래스와 유사하지만 동시성 문제를 방지하기 위해 특정 스레드만 접근하도록 제한한다

이를 통해 데이터 경쟁(race condition)과 같은 문제가 발생하지 않도록 보호하지만, 클래스보다 약간의 오버헤드가 발생한다

actor는 클래스와 달리 다음과 같은 사용 시 차이가 존재한다

 

  • 동시성과 관련된 내부의 속성이나 메서드에 접근 시, await 키워드 사용
  • 동시성과 관련이 없는 요소는 nonisolated 키워드를 사용해 비동기 접근 제한

다음 예시코드는 timeStamps을 공유 자원으로 동시에 접근하는 경우, actor를 이용한 예시다

actor를 사용하지 않으면 쓰기 도중 읽기를 허용하게 되어, 완전히 기록되지 않은 상태의 데이터를 읽게 되는 문제가 발생할 수 있다

 

actor TimeStore {
    var timeStamps: [Int: Date] = [:]

    func addStamp(task: Int, date: Date) {
        timeStamps[task] = date
    }
}

func doSomething() async {
    let timeStore = TimeStore()

    await withTaskGroup(of: Void.self) { group in
        for i in 1...5 {
            group.addTask { 
                await timeStore.addStamp(task: i, date: await takesTime()) 
            }
        }
    }

    for (task, date) in await timeStore.timeStamps {
        print("Task = \\(task), Date = \\(date)")
    }
}

@MainActor

SwiftUI의 경우, UI 랜더링은 main 스레드에서 이뤄지기에 관련 데이터도 main 스레드만 접근가능해야 한다

이때, @MainActor는 main 스레드에서 작업이 수행되게 해 준다

 

class ContentViewModel: ObservableObject {
    @Published var data: Data? = nil
    
    var someService = SomeService()

    func loadData() async {
        await someService.someAsync()
        let url = URL(string: "<https://example.com>")!
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            await updateData(data: data)
        } catch {
            print("Failed to load: \\(error)")
        }
    }
    
    @MainActor
    func updateData(data: Data) {
        self.data = data  // print(Thread.isMainThread) true
    }
}

 

@MainActor가 붙은 updateData 메서드는 다음 코드와 동일하다

await MainActor.run { self.data = data }

 

마무리

Swift Concurrency지 Swift Parallel이 아니다

CPU 작업을 비동기로 처리하려면 Task 생성 시, priority를 낮춰서 협력 스레드가 전부 점유되는 상황을 피해야 한다

'iOS+ > Swift' 카테고리의 다른 글

Codable Protocol  (0) 2025.01.15
URLSession을 이용한 네트워킹 작업  (0) 2025.01.12
ARC(Automatic Reference Counting)  (0) 2025.01.05
Transferable  (0) 2024.09.08
Swift 명령어와 Swift Package  (0) 2024.06.16