본문 바로가기
Xcode/Swift - PlayGround

Swift) Async - Await 정리하기 #2 (Async let, withTaskGroup, Task)

by 후르륵짭짭 2023. 6. 12.
728x90
반응형

어느 카페에서 찍은 사진

안녕하세요. 후루륵짭짭입니다.

이전에 Async Await에 대해서 체험판으로 정리한 적이 있었습니다.

2022.08.13 - [Xcode/Swift - PlayGround] - PlayGround) Async - Await 경험 정리#1

이때는 정말 체험판으로 작성 했다면, 이제는 좀 더 심도 있게 경험한 것들을 차례대로 정리나가려고 합니다.

정말 기본적인 것은 위에 정리하였고 오랜만에 다시 공부하면서 정리해보려고 합니다.

 

** Async - Await에 대한 정리 ** 

func calculateAPR(creditScores : [CreditScore]) -> Double {
    let sum = creditScores.reduce(0, {next, credit in
        return next + credit.score
    })
    return Double( (sum/creditScores.count) / 100)
}

func getAPR(userId: Int) async throws -> Double {
    
    guard let equifaxUrl = Constants.Urls.equifax(userId: userId),
          let experianUrl = Constants.Urls.experian(userId: userId) else {
              throw NetworkError.badUrl
          }

    //일반적인 await는 해당 메소드가 끝나고 나서 다음 것을 수행합니다.
    let (equifaxData, _) = try await URLSession.shared.data(from: equifaxUrl)
    print("equifaxData : \(equifaxData)")
    let (experianData, _) = try await URLSession.shared.data(from: experianUrl)
    print("experianData : \(experianData)")
    return 0.0
    
}

Await 함수는 해당 과정이 끝나고 나서 다음 단계로 넘어 갑니다.

따라서 eqifaxData의 Await가 끝나고 나서 experianData를 수행하고 이 experianData가 끝나야 return을 하게 됩니다.

그리고 당연하게 Await는 비동기인 Async에서 작동해야하죠.

 

그런데 만약 절차적으로 수행하기 보다는 병렬적으로 작업하고 싶은 경우에는 async let을 사용했습니다.

func getAPRAsynclet(userId: Int) async throws -> Double {

    if userId % 2 == 0 {
        throw NetworkError.invalidId
    }
    try Task.checkCancellation() //부모나 다른 Task들이 취소되었다면 더 이상 해당 로직을 수행할 필요가 없기 때문에 에러를 발생시킨다.

    guard let equifaxUrl = Constants.Urls.equifax(userId: userId),
          let experianUrl = Constants.Urls.experian(userId: userId) else {
              throw NetworkError.badUrl
          }

    //Async let을 사용하면 순서에 상관없이 먼저 온 것을 수행한다.
    (1) async let (equifaxData, _) = URLSession.shared.data(from: equifaxUrl)
    (2) async let (experianData, _) = URLSession.shared.data(from: experianUrl)

    //async let 이기 때문에 일단 비동기 적으로 수행을 하고 try await가 아래에 있기 때문에 해당 구문을 수행하기 전에는 experianData 결과가 있어야한다.
    (3) let experianCreditScore = try JSONDecoder().decode(CreditScore.self, from: try await experianData)
    print("experianCreditScore : \(experianCreditScore)")

    (4) let equifaxCreditScore = try JSONDecoder().decode(CreditScore.self, from: try await equifaxData)
    print("equifaxCreditScore : \(equifaxCreditScore)")

    return calculateAPR(creditScores: [experianCreditScore,equifaxCreditScore ])

}

이렇게 하면 (1) 끝나고 (2)를 수행하는 것이 아닌 (1)과 (2)가 동시에 작동을 합니다.

그리고 (3)에서 experianData 작업이 끝났는지 확인하고 해당 작업이 끝나면 (4)으로 넘어가고 이 또한 끝나야 return을 하게 됩니다.

 

** Group Task ** 

기존에 사용하던 비동기 처리 함수 중 DispatchGroup이라는 것이 존재 했습니다.

이는 여러 비동기 처리를 기다렸다가 한번에 처리할 때 사용하는 겁니다. 

그런데 이것과 동일한 기능을 Async Await에서도 제공합니다.

struct ReturnValue : Sendable {
    let id : Int
    let score : Double
}

Task(operation: {

    var resultValues : [ReturnValue] = []

    for id in [1,2,3,4,5] {
        do{
            //이렇게 내부는 Concurrency로 작동하지만 상위가 Serial이라면 반복문읜 Serial로 작동한다.
            let apr = try await getAPRAsynclet(userId: id)
            resultValues.append(.init(id: id, score: apr))
        }
        catch{
            resultValues.append(.init(id: id, score: 0))
        }
    }

    print(resultValues)
})

이렇게 반복문안에 작업을 해도 결국에 한번에 모았다가 처리하게 됩니다.

왜냐하면 하나의 await 작업이 끝나야 다음 await 작업을 진행하기 때문입니다.

이렇다고 한다면 순차적으로 결과를 받아오기는 하지만 데이터 처리 속도가 늦어질 수 있습니다. 

FOR RESULT
[UnstructuredConcurrencyIntro.ReturnValue(id: 1, score: 7.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 2, score: 0.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 3, score: 8.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 4, score: 0.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 5, score: 7.0)]

위와 같이 1,2,3,4,5 순차적으로 결과가 나오게 됩니다.

 

반면 withTaskGroup이라는 것이 존재합니다.

func getAPRForAllUsers() async throws -> [ReturnValue] {

    var resultValues : [ReturnValue] = []

    try await withThrowingTaskGroup(of: ReturnValue.self, body: { group in
        for id in [1,2,3,4,5] {
            group.addTask(operation: {
                do {
                    let result = try await self.getAPRAsynclet(userId: id)
                    return .init(id: id, score: result)
                }
                catch {
                    return .init(id: id, score: 0)
                }
            })
        }

        for try await result in group {
            resultValues.append(result)
        }
    })

    return resultValues
}

이는 DispatchGroup과 동일합니다. 

DispatchGroup도 group.enter 와 group.leave를 통해서 모음을 만들어 줍니다 .

이것도 addTask로 수행할 작업을 넣어주고 

마지막에 group을 await로 기다리면 해당 결과가 반환이 됩니다 .

TASKGROUP RESULT
[UnstructuredConcurrencyIntro.ReturnValue(id: 2, score: 0.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 4, score: 0.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 3, score: 7.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 1, score: 7.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 5, score: 6.0)]

그렇다면 위와 같이 결과가 출력이 되는데 순서가 보장되지 않습니다.

따라서 병렬적으로 작업하고 모든 과정이 다 끝나면 결과를 반환하게 됩니다 .

여기서 Sendable 프로토콜의 개념이 등장하게 됩니다. 

해당 이미지를 클릭하면 Apple Document로 이동합니다.

withTaskGroup(of : sendableProtocol)이 들어가게 되는데, 

SendableProtocol은 값이 비동기적으로 작업할 때 Thread Safe(Data race 방지)하게 값을 전달하기 위해서 존재하는 겁니다.

따라서 보통 Struct와 같은 Value 타입에 적용이 됩니다.

자세한 내용은 여기에 있습니다.

https://zeddios.tistory.com/1305

 

Actor (4) - Sendable

안녕하세요 :) Zedd입니다. 오늘은 Sendable에 대해 공부! # Actor 자 여러분 actor 아시죠!? actor BankAccount { } 이렇게 actor 타입을 만들었었잖아요! 근데 Actor라는 것도 있어요. 얘는 뭘까요? Actor는 프로토

zeddios.tistory.com

 

** Unstructured Concurrency Task ** 

지금 까지 배운 것은 절차적으로 수행 되는 Structured Concurrency라고 불리더라구요.

반면에 절차적으로 수행 되지 않고 사용자의 필요에 따라서 취소가 가능하고 호출해주는 Unstructured Concurrency인 

Task라는 것이 존재합니다 .

위에서도 이미 예시로 나왔는데, async 함수를 호출해주기 위해서는 Task 공간 안에서 호출이 되어야 합니다.

 

Task는 사용자의 필요에 따라 결과 값을 가져올 수 있습니다.

var aprTask : Task<[ReturnValue],Error>? {
    didSet {
        if aprTask == nil {
            APRResult.text = "Download"
        }
        else {
            APRResult.text = "Canceled"
        }
    }
}

예를들어 위와 같이 Task를 에러가 존재하면 결과 값으로 [ReturnValue] 값을 반환하는 property를 설정한다면 

aprTask = Task(operation: {
    let result = try await self.getAPRForAllUsers()
    return result
})

이렇게 Task Property에 Task 작업을 저장할 수 있게 됩니다.

Task(operation: {
    do{
        let result = try await aprTask?.value
        print("TASKGROUP RESULT")
        print(result ?? [])
    }
    catch {
        print("resultError : ",error.localizedDescription)
    }
})

그리고 위와 같이 await를 통해서 저장한 Task를 호출해주고 작업이 끝날 때 까지 기다린다음 value를 통해서 값을 가져옵니다.

만약 해당 Task를 취소하고 싶다면 아래 처럼 취소가 가능합니다.

aprTask?.cancel()

이렇게 Task는 사용자의 필요에 따라서 자유롭게 호출 및 취소가 가능하여 Unstructured Concurrency 라고 불린다고 합니다.

 

지금까지 그냥 입맛대로의 Async - Await에 대해 정리해봤습니다.

알아 볼 수 있을지 모르겠지만 ㅠㅠ 

도움이 됐으면 합니다.

 

** 참고 사이트 ** 

https://sujinnaljin.medium.com/swift-actor-%EB%BF%8C%EC%8B%9C%EA%B8%B0-249aee2b732d

 

[Swift] Actor 뿌시기

근데 이제 async, Task 를 곁들인..

sujinnaljin.medium.com

 

병렬적으로 작동하는 detached-task

https://www.avanderlee.com/concurrency/detached-tasks/

 

Detached Tasks in Swift explained with code examples

Detached tasks allow you to run code asynchronously, but you should be aware of the substantial consequences they introduce.

www.avanderlee.com

 

Sendable에 대한 정리 

https://zeddios.tistory.com/category/Swift/Concurrency

 

'Swift/Concurrency' 카테고리의 글 목록

iOS Developer. Swift lover. Xcoder. Be truth seekers

zeddios.tistory.com

 

StructuredConcurrency

https://www.andyibanez.com/posts/structured-concurrency-with-group-tasks-in-swift/

 

Structured Concurrency With Task Groups in Swift

Learn about executing a dynamic amount of concurrency in Swift using Task Groups.

www.andyibanez.com

https://www.andyibanez.com/posts/structured-concurrency-in-swift-using-async-let/

 

Structured Concurrency in Swift: Using async let

Get started using structured concurrency in Swift using async let tasks.

www.andyibanez.com

https://www.andyibanez.com/posts/unstructured-concurrency-with-detached-tasks-in-swift/

 

Unstructured Concurrency With Detached Tasks in Swift

Learn to use detached tasks in Swift for concurrency and why they are useful.

www.andyibanez.com

 

728x90
반응형

댓글