[Concurrency] TaskGroup

정유진·2022년 7월 21일
0

swift

목록 보기
6/24
post-thumbnail

🧸 async 의 연장선으로...

Swift 5.7 매뉴얼에서 Concurrency 챕터에서는 Task and Task Group 이라는 개념이 소개되고 있는데 오키 내용은 알겠고 어떻게 쓰라고? 하다가 이거 나중에 프로젝트에서 써먹어야지 하는 생각으로 기록하는 예시 코드이다.

🎼 간단하게 TaskGroup 개념 짚기

  • task는 비동기적으로 run 될 수 있는 코드들의 단위 (unit으로 이해)
  • 모든 비동기 코드는 task의 일부로서 run 된다.
  • 이전에 소개했던 async-let은 child task를 제공하기 때문에 async 함수를 사용할 수 있었던 것! (async는 task 안에서 사용할 수 있다)
  • 여기에 더해 taskGroup 을 사용하면 그 taskGroup에 child task를 "add" 하여서 개발자가 task의 우선순위와 task의 cancellation을 통제할 수 있다.
  • taskGroup 안의 task들은 공통의 parent task를 가지고 각각의 task는 역시 child task를 가질 수 있다. (tree를 상상해보자!)

https://developer.apple.com/documentation/swift/taskgroup
자세한 내용은 공식문서를 참고!

🧐 Function 미리보기

withTaskGroup(of:body:)

class TaskGroupTest {
    func test() async {
        // starts a new scope that can contain a dynamic number of child task
        await withTaskGroup(of: Data.self) { taskGroup in
            let photoNames = await listPhotos(inGallery: "Summer Vacation")
            for name in photoNames {
                taskGroup.addTask {
                    await self.downloadPhotos(name: name).data(using: .utf8)!
                }
            }
        }

    }
  • taskgroup을 통해 시간이 소요되는 작업(task)이 비동기적으로 수행되게 하고 task가 모두 끝날 때까지 await한 후에 그 결과물로 이어 작업을 할 수 있다.
  • 여기서 핵심은 withTaskGroup 함수!
  • of: 에는 각 child task가 반환하는 결과값의 Type을 넘긴다.
  • body 부분에는 아래와 같은 closure가 들어가는데 위의 코드와 함께 보면, closure의 param에는 taskGroup이 넘어오므로 이 taskGroup 안에 내가 원하는 task를 addTask 하면 그 child task들이 착착 실행된다.
(inout TaskGroup<ChildTaskResult>) async -> GroupResult)

https://developer.apple.com/documentation/swift/withtaskgroup(of:returning:body:)
자세한 내용은 역시나 공식문서 참고가 최고!

  • addTask를 좀더 자세히 살펴보자.

addTask(priority:operation:)

mutating func addTask(
    priority: TaskPriority? = nil,
    operation: @escaping () async -> ChildTaskResult
)
  • addTask의 operation 안에서 "비동기적으로 처리되기 원하는, 복잡하고 시간이 소요되는 작업"을 수행하면 된다.
  • 주의할 점이 있다면 childTaskResult로 반환되는 값의 type이 당연히 of: 의 type 과 동일해야 한다는 것이다!
  • 위의 샘플 코드의 경우 Data가 반환되는 것을 확인할 수 있다.

나의 경우에는, 공식문서를 봐도 그래서 저 withTaskGroup의 결과물을 어떻게 써먹어야 할지 감이 오지 않았다. 공식 문서는 나를 너무 고평가한 것이다! 그래서 나는 내가 이해할만한 쉬운 예시를 만들어 보았다.

🧩 Sample Code

1) 예시 1 basic

 func printMessage() async {
        let words = ["Hello", "My", "name", "is", "yujinj"]
        let string = await withTaskGroup(of: String.self) { taskGroup -> String in
            for word in words {
                taskGroup.addTask {
                    // 시간이 소요되는 작업을 진행 
                    // let data = await requestToServer(word)
                    // 서버와 통신 후 결과 데이터 반환 (여기서는 그냥 word로 퉁친다.)
                    return word + " (by Server) "
                }
            }
            
            var collection = [String]()
            
            // taskGroup을 결과값이 들어있는 리스트라고 생각한다.
            for await value in taskGroup {
                collection.append(value)
            }
     
            return collection.joined(separator: " ")
        }
        
        print("said:", string)
    }
  • words안의 word 하나하나가 내가 서버에 던질 request라고 가정해보자.
  • 나는 taskGroup에 내가 수행하기 원하는 child task를 add 하였다.
  • child task의 내용: 나는 word 하나를 requset로 던졌고 그에 대한 결과물을 받음 (가정)
  • taskGroup을 child task의 결과 하나하나가 담겨있는 리스트라고 이해하자.
  • task 수행이 완료되고 value를 반환받기 까지는 시간이 걸리기 때문에 value 앞에 await를 붙였다.
  • 결과 하나하나를 빈 배열에 append하고 그 배열을 joined 하여서 return한다면 그 result가 let string에 담길 것이다.
  • string을 print 한다면 무슨 문장이 출력될까? word가 뒤섞일까? (무슨 정처기 문제같네)
"said: Hello (by Server)  My (by Server)  name (by Server)  is (by Server)  yujinj (by Server)"

2) 예시 2 + UI update

func sumNumbers() async {
        let numbers = [1, 2, 3, 4, 5]
        
        let total = await withTaskGroup(of: Int.self) { taskGroup -> Int in
            for num in numbers {
                taskGroup.addTask{ 
                // 복잡하고 시간이 걸리는 로직이 진행된다...
                return num 
                }
            }
           
            let reduceResult = await taskGroup.reduce(0,+)
            return reduceResult
        }
        
        await MainActor.run {
        	// UI update
    	}
        
        print("total", total)
    }
  • taskGroup 로직 자체는 위와 다르지 않다.
  • UI를 업데이트 해야할 경우 DispatchQueue.main.async 외에도 MainActor를 사용할 수 있다.

🥳 결론

async-await와 함께 사용하면 더 시너지를 발휘할 taskGroup! 다음에 회사에서 이렇게 써봐야지 하는 생각을 하고 있다. DispatchGroup과 비슷하게 사용할 수 있을 것 같고 task의 cancellation에 대해서는 다음에 다시 살펴보겠다.

profile
느려도 한 걸음 씩 끝까지

0개의 댓글