본문 바로가기
Xcode/Swift - PlayGround

PlayGround) Actor에 대해 경험한 것 적어보기(feat: Task, Async Await)

by 후르륵짭짭 2023. 8. 13.
728x90
반응형

충무로 어디선가 만난 빵

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

태풍이 지나갔네요. 

그래서 재택 근무를 연속으로 해서 그런지 개인적인 시간이 많아서 좋았습니다. 

사이드 프로젝트를 해야하는데, 마음이 쉽지 않네요 ㅠ ㅠ.

이번 글은 그냥 제가 경험한 Async Await와 Actor 그리고 Sendable 그리고 MainActor에 대해서 

주관적으로 의식의 흐름 기법으로 작성해보려고 합니다. 

 

** Actor ** 

일단 Actor는 Swift 5.5 버전에서 부터 지원되는 비동기 프로그램의 안정성을 위해서 나온 것 입니다.

사내 프로젝트를 진행하다가 비동기 작업으로 race condition이 걸린 적이 있어서 앱이 죽은 적이 있습니다.

그 때 reference type의 값을 여러 쓰레드에서 접근하게 되어 죽은 적이 있어서 

barrior를 둬서 해결한 적이 있습니다. 

이러한 문제를 해결하기 위해서 Actor라는 것이 등장 했습니다.

 

Actor는 Async - Await와 함께 사용할 때 엄청난 시너지를 발생하는데, 

Thread 안정성을 위해 사용한 객체입니다. 

예를들어 아래와 같이 Reference Type인 NumberContainer가 있다고 합시다.

class NumberContainer {
    var headNumber : CGFloat = 0.0
    var footNumber : CGFloat = 0.0
}

그리고 actor 인스턴스를 아래와 같이  선언 해줍니다.

actor NumberManager  {

    var numberContainer : NumberContainer

    init() {
        self.numberContainer = .init()
    }

    func changeFootNumber(num : Int) async throws -> CGFloat {
        let secton = Int.random(in: 0...3)
        numberContainer.footNumber = CGFloat.random()
        print("\(num) / \(secton) : ",numberContainer.footNumber)
        return numberContainer.footNumber
    }

}

그리고 아래 코드에서 호출을 해줍니다.

class ImageViewModel : ObservableObject {
   
    @Published var footNumber : CGFloat = 0.0
    private let manager = NumberManager()
    
    @MainActor func generateNumber() async {

            await withTaskGroup(of: CGFloat.self, body: { group in
                for num in 0..<20 {
                    group.addTask {
                        do{
                            return try await self.manager.changeFootNumber(num: num)
                        }
                        catch {
                            return 0.0
                        }
                    }
                }

                await group.waitForAll()

                self.footNumber = await self.manager.numberContainer.footNumber

    })
}

위 코드를 보면 changeFootNumber 메소드를 20번 순서에 상관없이 작업하는 것을 확인 할 수 있고 

모든 작업이 끝날 때까지 기다린 다음 footNumber를 갱신해줬습니다.

0 / 2 :  0.046511753240253716
1 / 3 :  0.997166680869918
2 / 1 :  0.19489115807108842
3 / 1 :  0.30939537456943544
4 / 2 :  0.4346545551518571
5 / 2 :  0.35133275979928036
6 / 3 :  0.3985870560627866
7 / 1 :  0.6635130130833744
8 / 1 :  0.15875084655330304
9 / 0 :  0.04593312461998619
10 / 0 :  0.588740940808491
11 / 3 :  0.6096908202417406
12 / 2 :  0.07507817588631953
13 / 3 :  0.7006825014717604
15 / 1 :  0.05652278011118127
16 / 2 :  0.3432689223772075
17 / 1 :  0.05902581663313923
18 / 0 :  0.6100788010773432
19 / 2 :  0.885746239890751
14 / 1 :  0.6985381773436764

그리고 마지막 숫자인 0.698이 최종 결과값이 되었습니다. 

또한 모든 숫자가 겹치지 않고 잘 출력되고 있습니다.

하지만 이것을 말하려는 것은 아니고 일반적인 class Object와 비교했을 때의 결과를 보려고 합니다.

기본적으로 Actor는 Async - Await를 기본으로 하기 때문에, 

메소드 사용, 프로퍼티의 값 가져올  때 await를 붙게 되어 있습니다. 

그래서 해당 결과값이 나와야지 다음 작업을 시작하게 됩니다. 

class NumberGenerator {

    var numberContainer : NumberContainer

    init() {
        self.numberContainer = .init()
    }

    func changeFootNumber(num : Int) async throws -> CGFloat {
        let secton = Int.random(in: 0...3)
        numberContainer.footNumber = CGFloat.random()
        print("\(num) / \(secton) : ",numberContainer.footNumber)
        return numberContainer.footNumber
    }

}

똑같은 코드인데 Actor 대신에 위 코드를 사용하면 어떻게 결과가 나올까요?

1 / 0 :  0.33355887591223204
0 / 3 :  0.33355887591223204
2 / 2 :  0.18997522471239214
3 / 2 :  0.17133059612739146
4 / 2 :  0.03507332714159818
13 / 3 :  0.27463435923555735
14 / 1 :  0.4939611557624212
15 / 0 :  0.17525742719305154
7 / 2 :  0.7318122204700047
9 / 0 :  0.874045431584596
10 / 3 :  0.7074041512579201
11 / 3 :  0.1526078479719832
12 / 1 :  0.45276581087446904
5 / 3 :  0.8671447236247232
16 / 2 :  0.8833533888411134
6 / 3 :  0.9801776006306003
17 / 0 :  0.7288745727224449
8 / 3 :  0.6719190330877711
18 / 2 :  0.5626648547040916
19 / 0 :  0.7602004273236265

종종 겹치는 숫자가 발생합니다. 

이런 경우 잘 못 해서 동시에 write를 하게 된다면 race condition이 발생해서 앱이 죽게 됩니다. 

 

- Actor의 프로퍼티의 값을 외부에서 변경하려고 할 때 -

만약에 아래 처럼 ColorManager 라는 Actor에 프로퍼티를 추가 했다고 합시다.

actor ColorManager  {

    var currentColor : UIColor = .black
    
 }

외부에서 해당 프로퍼티 값을 수정하려고 할 때 non-isolated 상태에서는 수정이 불가능합니다.

즉 자신의 Actor 내부에서 수정이 가능하다는 의미 입니다.

 

** Task를 이용하여 원하는 시점에 값 가져오기 ** 

class ImageViewModel : ObservableObject {
    private var colorChangeTask : Task<Void, Error>?
    
    @MainActor func changeColor() {
        self.colorChangeTask = Task(operation: {
            try await Task.sleep(for: .seconds(3))
            print("Task Start")
            self.subViewColor = Color(uiColor:  await manager.changeColorRandom())
            changeColor()
        })
    }
    
    func endTask() {
        self.colorChangeTask?.cancel()
        self.colorChangeTask = nil
    }  
    
}

이전에도 Task에 대해 다룬적이 있었던거 같습니다. 

https://hururuek-chapchap.tistory.com/245

 

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

안녕하세요. 후루륵짭짭입니다. 이전에 Async Await에 대해서 체험판으로 정리한 적이 있었습니다. 2022.08.13 - [Xcode/Swift - PlayGround] - PlayGround) Async - Await 경험 정리#1 이때는 정말 체험판으로 작성 했

hururuek-chapchap.tistory.com

위와 같이 코드를 작성한다면 

Task에 Sendable 데이터로 Void가 전달이 되고 Error 타입이 존재하게 됩니다.

self.colorChangeTask = Task(operation: {
            try await Task.sleep(for: .seconds(3))
            print("Task Start")
            self.subViewColor = Color(uiColor:  await manager.changeColorRandom())
            changeColor()
        })

이렇게 Task.sleep로 Delay를 주고 해당 Actor로 부터 Random Color가 나오게 되면 그것을 subViewColor에 적용해 줬습니다.

그리고 해당 작업이 끝나면 반복적으로 작업을 하게 했습니다.

이렇게 Task를 Property에 넣어준다면 원핫는 시점에 Value를 가져올 수도 있고 해당 Task를 제거할 수도 있게됩니다.

 

** 참고 사이트 **

Actor에 대해서 : 

https://medium.com/@gitaeklee/swift-actor-with-sample-930f8cf1ab09

 

Swift Actor with Sample

Swift actors are a new concurrency feature introduced in Swift 5.5 that provide a safe and efficient way to manage shared mutable state…

medium.com

https://zeddios.tistory.com/1290

 

Swift ) Actor (1)

안녕하세요 :) Zedd입니다. WWDC 21 ) What‘s new in Swift 에서도 잠깐 본 내용인데, Actor에 대해서 공부. # 다중 쓰레드 시스템에서 제대로 작동하지 않는 코드 WWDC 21 ) What‘s new in Swift 에서 본 예제. class

zeddios.tistory.com

 

Sendable : 

https://www.donnywals.com/what-are-sendable-and-sendable-closures-in-swift/

 

What are Sendable and @Sendable closures in Swift? – Donny Wals

One of the goals of the Swift team with Swift’s concurrency features is to provide a model that allows developer to write safe code by default. This means that there’s a lot of time and energy…

www.donnywals.com

 

Task에 Delay 주는 방법 :

https://www.swiftbysundell.com/articles/delaying-an-async-swift-task/

 

Delaying an asynchronous Swift Task | Swift by Sundell

How we can use the built-in Task type to delay certain operations when using Swift’s new concurrency system.

www.swiftbysundell.com

https://stackoverflow.com/questions/68744610/recurring-function-in-swift-5-5-using-async-await

 

Recurring function in Swift 5.5 using async/await

I want to keep firing a function 5 seconds after it completes. Previously I would use this at the end of the function: Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { self.function() } ...

stackoverflow.com

 

Task의 내용 정리 :

https://green1229.tistory.com/332

 

Swift Concurrency - Task (1)

안녕하세요. 그린입니다🍏 이번 포스팅부터는 Swift Concurrency에 대해 체계적으로 학습해보려해요🙋🏻 그래서 제목도 이번이 처음 (1)을 붙였습니다! 앞으로 해볼 학습들은 다 아래 레퍼런스 토

green1229.tistory.com

 

Async - Await Unit Test :

https://holyswift.app/guide-to-unit-testing-with-async-await-in-swift/

 

Guide to Unit Testing with Async/Await in Swift - Holy Swift

Discover three ways to use the new async/await APIs to unit test your code. Start testing your async functions today!

holyswift.app

 

728x90
반응형

댓글