[Swift] Actor 뿌시기

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

들어가기 전에

ㅋㅋ 진짜 수특도 이렇게는 안했었다구요,,,
웃겨.. 아니 안웃겨..

비동기 코드

  1. 해당 비동기 코드는 바로 실행 되는 것도 아니고
  2. 이후 라인에 있는 코드 실행을 막지도 않습니다
  1. 비동기 코드는 나중에 알 수 없는 시간에 호출될 수 있는 코드이기 때문에, 해당 라인에 도달해도 바로 실행되지 않음
  2. 이후 라인에 있는 코드 실행을 막지도 않음
직-관

async/await

애플 워치처럼 CPU 가 두개인 듀얼 코어 상황을 가정해볼게여
  1. CPU 코어의 수만큼스레드를 만들고
  2. 스레드를 차단하지 않고도, 저렴하고 효율적으로 작업을 전환할 수 있도록 하는 것입니다.

Task

  • 값 타입
  • mutable storage 가 없는 참조 타입
  • 내부적으로 상태에 대한 액세스를 관리하는 참조 타입
  • @Sendable로 표시된 함수 및 클로저
  • 격리되어있고 (isolated), 독립적으로(independently) 비동기 작업을 수행합니다.
  • 값이 공유될 상황이 있을때는 Sendable 체킹을 통해 Task 가 격리된(isolated) 상태로 남아있는지 체크합니다

Actor

actor MyActor {}
actor SharedWallet {
let name = "공금 지갑"
var amount: Int = 0

init(amount: Int) {
self.amount = amount
}

func spendMoney(ammount: Int) {
self.amount -= ammount
}
}
Task {
let wallet = SharedWallet(amount: 10000)
let name = wallet.name // 1. 상수는 변경 불가능하기 때문에 어느 스레드에서 접근해도 안전함. actor 외부에서도 바로 접근 가능
let amount = await wallet.amount // 2. actor 외부에서 변수 접근시 await 필요
await wallet.spendMoney(ammount: 100) // 3. actor 외부에서 메서드 호출시 await 필요
await wallet.amount += 100 // 4. ❌ 컴파일 에러. actor 외부에서 actor 내부의 변수를 변경할 수 없음
}

Actor serialization

public protocol Actor: AnyObject, Sendable {
nonisolated var unownedExecutor: UnownedSerialExecutor { get }
}
@available(SwiftStdlib 5.5, *)
public protocol Executor: AnyObject, Sendable {
func enqueue(_ job: UnownedJob)
}
@available(SwiftStdlib 5.5, *)
public protocol SerialExecutor: Executor {
func asUnownedSerialExecutor() -> UnownedSerialExecutor
}
func greet(_ friend: Chicken) {}
actor Island {
var flock: [Chicken]
}

extension Island {
func greetOne() {
if let friend = flock.randomElement() {
greet(friend)
}
}
}
func greetAny(flock: [Chicken]) async {
if let friend = flock.randomElement() {
greet(friend)
}
}

Actor reentrancy

actor ImageDownloader {
private var cache: [URL: Image] = [:]
func image(from url: URL) async throws -> Image? {
// 캐시 체크
if let cached = cache[url] {
return cached
}
// 이미지 다운로드
let image = try await downloadImage(from: url) // <-- suspension point
// 캐시에 이미지 저장
cache[url] = image
return image
}
}

Actor reprioritization

주의점 1. actor에서 await 호출 이후 내부 상태 가정 금지

actor MyDownloader {
var counter = 0
func download(url: URL) async throws -> Data {
counter += 1 // counter 1 올리고
let num = counter // counter 를 num 에 할당
let result = try await URLSession.shared.data(from: url) // await 로 URLSession 함수 호출

print("num : " + num.description + ", " + "counter : " + counter.description)
print(num == counter) // counter 를 그대로 num 에 할당했으니까 true.. 일까나?

return result.0
}
}
num: 1, counter: 2
false

num: 2, counter: 2
true
  1. 2 개 이상의 Task 에서 MyDownloader 에 접근하려고 함
  2. 먼저 접근한 Task 1 에서 counter 를 1 로 올리고, num 에 1 를 할당함. 그리고 await 에서 actor 의 실행이 중단됨
  3. actor 에서 실행되는 코드가 없는 틈을 타서 Task 2 가 들어옴
  4. Task 2counter 를 2 로 올리고 num 에 2 를 할당함. 그리고 await 에서 actor 의 실행이 중단됨
  5. Task 1 이 actor 에 접근해서 await 한 작업이 먼저 끝남. await 이후의 코드가 마저 실행될 때 num 은 1 이지만, counter 는 2 가 되어있음
  6. 따라서 num == counterfalse
  7. Task 2 가 actor 에 접근해서 await 한 작업이 끝남. await 이후의 코드가 마저 실행될 때 num 은 2, counter 는 2 가 되어있음.
  8. 따라서 num == countertrue
num: 2, counter: 2
true

num: 1, counter: 2
false
  1. 2 개 이상의 Task 에서 MyDownloader 에 접근하려고 함
  2. 먼저 접근한 Task 1 에서 counter 를 1 로 올리고, num 에 1 를 할당함. 그리고 await 에서 actor 의 실행이 중단됨
  3. actor 에서 실행되는 코드가 없는 틈을 타서 Task 2 가 들어옴
  4. Task 2counter 를 2 로 올리고 num 에 2 를 할당함. 그리고 await 에서 actor 의 실행이 중단됨 (여기까지는 위의 상황과 똑같죠?)
  5. Task 2 가 actor 에 접근해서 await 한 작업이 먼저 끝남. await 이후의 코드가 마저 실행될 때 num 은 2, counter 는 2가 되어있음.
  6. 따라서 num == countertrue
  7. Task 1 이 actor 에 접근해서 await 한 작업이 끝남. await 이후의 코드가 마저 실행될 때 num 은 1, counter 는 2 가 되어있음.
  8. 따라서 num == counterfalse

주의점 2. actor 에서 많은 양의 작업을 진행하지 않는다

Actor Hopping

파파고야 넘넘 고마운데!! 혹시 노력 조금만 더 해줄 수 있어?!? ️❤️
  1. actor 를 hopping 하면서 스레드는 block 되지 않았고 (아까 파파고가 번역한 문장이져 ㅋ..)
  2. hopping 은 다른 스레드를 필요로 하지 않았다는 점입니다.

Main Actor

DispatchQueue.main.async {}
await MainActor.run {}
await MainActor.run {
// UI 관련 코드 1
}
await MainActor.run {
// UI 관련 코드 2
}
await MainActor.run {
// UI 관련 코드 1
// UI 관련 코드 2
}
actor NaljinActor {
let id = UUID().uuidString
// property 에 붙으면, 해당 property 는 main thread 에서만 접근 가능
@MainActor var myProperty: String

init(_ myProperty: String) {
self.myProperty = myProperty
}

// method 에 붙으면, 해당 메서드는 main thread 에서만 호출 가능
@MainActor func changeMyProperty(to newValue: String) {
self.myProperty = newValue
}

func changePropertyToName() {
Task { @MainActor in
// block 안에 들어있는 코드도 main actor 에서 실행
myProperty = "naljin"
}
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
class ViewController: UIViewController {
func taskThreadTest() {
Task { // 아래 코드 블럭은 main thread 에서 동작
self.myTextLabel.text = "naljin"
}
}
}
<_NSMainThread: 0x600000b882c0>{number = 1, name = main}

마무리

출처

--

--

 https://github.com/sujinnaljin/TIL

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store