안녕하세요. 후르륵짭짭입니다.
태풍이 지나갔네요.
그래서 재택 근무를 연속으로 해서 그런지 개인적인 시간이 많아서 좋았습니다.
사이드 프로젝트를 해야하는데, 마음이 쉽지 않네요 ㅠ ㅠ.
이번 글은 그냥 제가 경험한 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
위와 같이 코드를 작성한다면
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
https://zeddios.tistory.com/1290
Sendable :
https://www.donnywals.com/what-are-sendable-and-sendable-closures-in-swift/
Task에 Delay 주는 방법 :
https://www.swiftbysundell.com/articles/delaying-an-async-swift-task/
https://stackoverflow.com/questions/68744610/recurring-function-in-swift-5-5-using-async-await
Task의 내용 정리 :
https://green1229.tistory.com/332
Async - Await Unit Test :
https://holyswift.app/guide-to-unit-testing-with-async-await-in-swift/
'Xcode > Swift - PlayGround' 카테고리의 다른 글
PlayGround) 팀 프로젝트에서 모듈화를 해야하는 이유 (0) | 2024.07.24 |
---|---|
Swift) AsyncStream 정리하기 (feat: Sequence, IteratorProtocol) (0) | 2023.07.24 |
Swift) Async - Await 정리하기 #2 (Async let, withTaskGroup, Task) (0) | 2023.06.12 |
PlayGround) PropertyWrapper와 Dependency Injection (0) | 2023.01.23 |
PlayGround) Combine 체험기#2 (0) | 2023.01.08 |
댓글