[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🍎 Apple/Concurrency & GCD

[Concurrency] Task

inu 2023. 3. 31. 20:35
반응형

Task

  • Task : 격리되어 있고 독립적인 비동기 작업 단위로, 코드 내에서 이를 활용하면 내부 코드는 비동기적으로 동작한다.
  • async 함수를 사용하기 위해서는 그 함수를 호출하는 컨텍스트 또한 동시성을 띄고 있어야 한다. (스레드가 제한된 상황에서는 처리 불가능)
  • 그러면 async를 호출하는 곳을 async로 만들고 이를 반복하다보면 결국엔 일반 메서드까지 도달할텐데 어떻게 처리해야 할까?
  • 이럴 때 사용하는 것이 Task이다. Task를 사용하면 명시적으로 동시성 컨텍스트를 생성할 수 있게 된다. 
  • 함수를 async로 표시하면 기본적으로 Task를 기반으로 한 동시성 컨텍스트를 사용하지만, 한 비동기 함수에서 다른 비동기 함수를 호출할 때는 여전히 동일한 Task(실행 컨텍스트)가 실행에 사용된다. 그러니 새로운 Task(실행 컨텍스트)가 필요할 경우 명시적으로 생성해주어야 한다.
  • return하여 값을 반환할 수도 있는데, 이를 통해 다른 Task의 컨텍스트와 데이터를 주고받을 수 있다.
    • 다만 이 때 값이 참조타입인 경우 data race로 인해 예상치못한 상황이 생길 수 있다.
    • 그래서 서로 다른 도메인끼리도 값을 전송시킬 수 있다는 의미인 'Sendable Protocol'을 지키는 타입만 사용되길 권장된다.

Task와 동시성코드

func test() async {
    print("1: \(Thread.current)")
    try? await Task.sleep(nanoseconds: 2_000_000_000)
    print("2: \(Thread.current)")
}

Task {
    print("0: \(Thread.current)")
    await test()
    print("3: \(Thread.current)")
}

// 0: <NSThread: 0x600000b80880>{number = 4, name = (null)}
// 1: <NSThread: 0x600000b80880>{number = 4, name = (null)}
// 2: <NSThread: 0x600000b9c3c0>{number = 6, name = (null)}
// 3: <NSThread: 0x600000b9c3c0>{number = 6, name = (null)}
  • Task 내부에서 async 함수를 호출하면 해당 작업을 하고 돌아오기 전까지는 같은 실행 컨텍스트를 사용한다. 굳이 여기서 스레드를 바꿔가며 작업을 처리할 이유가 없다. 따라서 함수 호출이후에도 사용하는 스레드는 그대로 이어지는 경우가 대부분이다. 오히려 내부의 async 함수가 동작하고, await를 통해 돌아오면 그 때 시스템은 다른 스레드를 할당하여 작업을 이어간다. continuation 객체의 도움으로 큰 무리없이 작업을 그대로 이어갈 수 있다.
func test() async {
    print("1-2: \\(Thread.current)")
    try? await Task.sleep(nanoseconds: 2_000_000_000)
}

Task {
    print("1-1: \\(Thread.current)")
    await test()
    print("1-3: \\(Thread.current)")
}

Task {
    print("2-1: \\(Thread.current)")
}

// 1-1: <NSThread: 0x6000018d1600>{number = 6, name = (null)}
// 1-2: <NSThread: 0x6000018d1600>{number = 6, name = (null)}
// 2-1: <NSThread: 0x6000018d1600>{number = 6, name = (null)}
// 1-3: <NSThread: 0x6000018d80c0>{number = 7, name = (null)}
  • Task가 연속으로 2개 존재할 경우 await할 동안 해당 스레드에 대한 점유를 시스템에 돌려주기 때문에 가능하다면 바로 같은 스레드로 다음 Task를 이어서 수행할 수도 있다. 위의 예시의 경우 시스템이 test 함수 내부의 1-2 Print문과 다음 Task의 Print문 처리를 같은 스레드로 처리했다.
    • 물론 이 사실보다는 async 함수를 호출해서 시스템이 작업을 동시적으로 처리될 수 있게 되었다라는 개념을 이해하는 것이 더 중요하다.

Task priority

  • Task에는 priority를 부여할 수 있다.
  • high = userInitiated > medium > low = utility > background 순서로 구성된다.
  • 다만 이는 종료시점을 보장하는 것이 아니다. 스레드가 작업의 우선순위를 판단할 때 보조적인 수단으로 사용될 뿐이다.
  • 따라서 이는 주로 보조적인 기능으로만 사용된다. (특정 작업이 반드시 우선시 될 필요는 없지만 되도록 먼저 처리되도록 구현하고 싶을 때 등)
Task(priority: .utility) {
    print("utility: \(Thread.current), \(Task.currentPriority)")
}

Task(priority: .userInitiated) {
    print("userInitiated: \(Thread.current), \(Task.currentPriority)")
}

Task(priority: .medium) {
    print("medium: \(Thread.current), \(Task.currentPriority)")
}

Task(priority: .low) {
    print("low: \(Thread.current), \(Task.currentPriority)")
}

Task(priority: .background) {
    print("background: \(Thread.current), \(Task.currentPriority)")
}

Task(priority: .high) {
    print("high: \(Thread.current), \(Task.currentPriority)")
}

// utility: <NSThread: 0x600001711b00>{number = 6, name = (null)}, TaskPriority(rawValue: 17)
// userInitiated: <NSThread: 0x60000171c100>{number = 5, name = (null)}, TaskPriority(rawValue: 25)
// medium: <NSThread: 0x60000170c540>{number = 4, name = (null)}, TaskPriority(rawValue: 21)
// low: <NSThread: 0x600001711f00>{number = 7, name = (null)}, TaskPriority(rawValue: 17)
// high: <NSThread: 0x60000171c100>{number = 5, name = (null)}, TaskPriority(rawValue: 25)
// background: <NSThread: 0x600001711f00>{number = 7, name = (null)}, TaskPriority(rawValue: 9)
반응형

'🍎 Apple > Concurrency & GCD' 카테고리의 다른 글

[Concurrency] Actor  (0) 2023.03.31
[Concurrency] Continuation  (2) 2023.03.31
[Concurrency] async / await  (2) 2023.03.31
[Swift] GCD 정리하기  (2) 2022.05.30
[Swift] Operation Queue  (0) 2022.05.27