기어가더라도 제대로

[iOS-더 나아가기] withTaskGroup - 동시적으로 async 코드 블럭 실행 본문

Swift - 더 나아가기

[iOS-더 나아가기] withTaskGroup - 동시적으로 async 코드 블럭 실행

Damagucci-juice 2023. 2. 2. 18:32

Operation

struct SlowDivideOperation {
    let name: String
    let a: Double
    let b: Double
    let sleepDuration: UInt64

    func execute() async -> Double {
        do {
            // Sleep for x seconds
            try await Task.sleep(nanoseconds: sleepDuration * 1_000_000_000)
            let value = a / b
            return value
        } catch {
            return 0.0
        }
    }

}

let operations = [
    SlowDivideOperation(name: "operation-0", a: 5, b: 1, sleepDuration: 5),
    SlowDivideOperation(name:  "operation-1", a: 14, b: 7, sleepDuration: 1),
    SlowDivideOperation(name: "operation-2", a: 8, b: 2, sleepDuration: 3)
]
  • excute() async -> Double : 간단한 나눗셈 작업을 sleepDuration 초 동안 기다린 후에 완료
  • operations : 작업 구조체들의 배열입니다.
    • 이 작업들을 동기적(Serial)으로 실행할지 동시적(concurrency)으로 실행할지 간단한 실험을 하나 해보겠습니다.
    • 순차적 작업 예상 소요 시간: 5 + 1 + 3 = 9초입니다.
    • 동시적 작업 예상 소요시간: 가장 긴 작업이 걸리는 시간인 5초 입니다.

순차적 실행(Serial)

//MARK: - 동기적 코드 실행
Task {
    print("Start Task!")
    let start = Date.timeIntervalSinceReferenceDate

    for operation in operations {
        let value = await operation.execute()
        print("\(operation) is Done!")
        let middle = Date.timeIntervalSinceReferenceDate
        print(String(format: "Duration: %.2fs", middle-start))
    }

    let end = Date.timeIntervalSinceReferenceDate
    print("End Task!")
    print(String(format: "Duration: %.2fs", end-start)) 
}
  • operation 이 완료될 때마다 시간이 출력되고 전체 작업이 모두 종료되면 End Task 이후에 모든 시간이 출력됩니다.
  • 작업 순서는 0번, 1번, 2번 순으로 완료가 되네요. 작업을 호출한 순서대로입니다.
  • 총 소요시간은 9.59초
동기적 실행

비동기적 실행(Concurrency)

//MARK: - 동시적 코드 실행
Task {
    print("Start Task!")
    let start = Date.timeIntervalSinceReferenceDate

    let allResults = await withTaskGroup(of: (String, Double).self, // child task return type
                                         returning: [String: Double].self, // group task return type
                                         body: { taskGroup in

        // Loop through operations array
        for operation in operations {
            // Add child task to task group
            taskGroup.addTask {
                // Execute slow operation
                let value = await operation.execute()
                print("\(operation) is Done!")
                let middle = Date.timeIntervalSinceReferenceDate
                print(String(format: "Duration: %.2fs", middle-start))
                // Return child task result
                return (operation.name, value)
            }
        }

        // Collect results of all child task in a dictionary
        var childTaskResults = [String: Double]()
        for await result in taskGroup {
            // Set operation name as key and operation result as value
            childTaskResults[result.0] = result.1
        }

        // All child tasks finish running, thus task group result
        return childTaskResults
    })

    let end = Date.timeIntervalSinceReferenceDate
    print("End Task!")
    print(String(format: "Duration: %.2fs", end-start))
}
  • withTaskGroup 이 돌면서 동시적으로 작업을 수행합니다.
  • 작업 완료는 1번,2번, 가장 오래걸리는 0번 순으로 되었습니다.
  • 전체 완료 시간은 5.34초

withTaskGroup 사용법

개수가 변하는 자식 Task를 포함하는 범위를 동작시킵니다.(??)

Discussion

A group waits for all of its child tasks to complete or be canceled before it returns. After this function returns, the task group is always empty.

To collect the results of the group’s child tasks, you can use a for-await-in loop:

모든 자식 Task가 작업 완료되기를(혹은 취소되기를) 기다리는 그룹입니다. 함수가 반환이 되면, TaskGroup은 항상 비어있습니다.

그룹의 자식 Task 의 작업 결과를 얻기 위해서, for-await-in반복문을 사용하세요.

  • of 파라미터: 자식 Task 가 완료한 작업의 결과의 타입을 나타냅니다.
  • returning 파라미터: 전체 그룹이 완료되었을 때 반환하는 타입을 나타냅니다.
  • 모두 메타타입으로 적혀있기 때문에 실제로 작성할 땐 SomeType.self 로 작성해야합니다.
  • 우리가 작성할 것은 Body 안쪽의 내용입니다. 두가지 작업을 합니다.
    • 모든 그룹의 자식 프로세스를 동시적으로 실행
    • 자식 테스크가 실행을 완료한 결과를 수확(?)
  • async 키워드가 Body 안에서 한번 나오고, 전체의 리턴타입 전에도 한번 나오니까 await 는 최소 2번 나와야겠습니다.
    • 실제로는 3번 나옵니다.
    • withTaskGroup 실행할 때 한번
    • for 문 안에서 비동기적인 자식 Task를 실행하면서 한번
    • Body안에서 결과를 얻을 때 한번
Task {
    let allResults = await withTaskGroup(of: (String, Double).self
                                       returning: [String: Double].self,
                                       body: { taskGroup in
            ~~~ 
    })
}
  • Task 안에서 실행이 되어야하고, await 키워드를 붙여줍니다.
  • of: 파라미터에는 자식 Task가 완료되었을 때 반환할 타입을 나타냅니다
  • returning: withTaskGroup이 반환할 타입을 나타냅니다.
let allResults = await withTaskGroup(of: (String, Double).self,
                                         returning: [String: Double].self,
                                       body: { taskGroup in

    for operation in operations {
      taskGroup.addTask {
          let value = await operation.execute()
          return (operation.name, value)
      }
    }

    var childTaskResults = [String: Double]()
  for await result in taskGroup {
      childTaskResults[result.0] = result.1
  }
  return childTaskResults
})

  • 노란색 테두리는 자식 Task의 개별 반환 타입을 의미하며
  • 빨간색 테두리는 withTaskGroup의 전체 반환 타입을 의미합니다.
  • await는 3번 나오네요.
    • 비동기적으로 작업을 수행하기 위해서 표시한 것이라는 점을 기억해야합니다.
    • 동기적으로 실행하는 코드가 아닙니다. 

빨간색 테두리와 동일한 타입으로 잡히는 것을 확인할 수 있습니다.

결론

순서가 중요하지 않고 전체 걸리는 시간이 중요하다면 withTaskGroup 으로 동시적으로 코드를 사용

출처

 

Comments