Actor (1)

French Marigold·2024년 3월 1일
3

iOS

목록 보기
3/3

GCD 를 이용한 비동기 처리

  • 어떤 작업을 비동기 처리한 후 “비동기 처리 완료의 시점을 파악”하기 위해서 보통 completionHandler를 사용했었다.
func printNumbers() {
    print("1")
    asyncFunction {
		// 2. Async 함수가 끝날 때 **"해당 클로저 안에 있는 코드가 실행"**되면서 
		// 사용자는 비동기 코드가 끝났다는 것을 확인할 수 있었음.
        print("비동기 코드가 끝났습니다.") 
    }
    print("3")
}

func asyncFunction(completionHandler: @escaping () -> Void) {
	// 전형적인 GCD를 이용한 비동기 처리 방법 ⭐️⭐️
    DispatchQueue.main.async {
        print("2")
        completionHandler() // 1. 비동기 코드가 끝나는 시점에 completionHandler를 호출
    }
}

printNumbers()
  • 근데 CompletionHandler는 다음과 같은 문제가 존재한다.
    1. CompletionHandler 호출을 실수로 까먹을 수 있음.
    2. 비동기 코드가 많을 경우, CompletionHandler를 무한정으로 콜백하는 콜백 지옥에 빠질 수 있음. ⭐️
    3. 가장 큰 단점은 CompletionHandler는 가독성이 매우 떨어진다는 것. ⭐️⭐️

async / await

  • CompletionHandler의 단점을 보완하고자 새로운 Swift Concurrency인 async / await이 도입되었다.
  • 함수 이름 뒤에 async가 붙으면 해당 함수는 비동기로 동작하는 함수를 의미한다.
  • 비동기 함수를 호출하려면 반드시 await를 붙여서 호출해야 한다.
  • 여기서 await라는 말은 현재 머물러 있는 “스레드에 대한 제어권을 잠시동안 포기하고 시스템에게 넘긴다는 의미”이다. 예를 들어, await 함수가 A 스레드에서 호출되었을 때 await 함수는 제어권을 시스템에게 넘겨주고 시스템이 알아서 추후에 해당 함수를 적절하게 실행될 수 있게끔 맡긴다는 거다. ⭐️⭐️
  • 그런데 시스템이 알아서 await 함수를 적절히 실행한다는 것은 시스템이 기존 스레드가 아닌 다른 스레드에서 await 함수 실행할 수도 있다는 것이다. 예를 들어, await 함수가 A 스레드에서 호출되면 await 함수가 시스템에게 제어권을 넘긴 후 시스템은 적절한 시점에 await 함수를 B 스레드에서 호출시킬 수 있다는 것이다. ⭐️⭐️
  • async / await을 사용하려면 반드시 Task라는 비동기 작업을 나타내는 Context와 함께 사용되어야 한다. 반드시 이 안에서만 await 함수를 호출한다.
  • Task 내부에서는 코드가 순차적으로 진행된다. ⭐️⭐️
// Task 내부에서는 코드가 순차적으로 진행된다. ⭐️⭐️
Task {
	let fish = await catchFish()
	let dinner = await cook(fish)

	await eat(dinner) 
}

기존 GCD와 Swift Concurrency(Async / Await) 의 동시성 처리 방법 차이점

  • GCD ⇒ 필요한 Task만큼 “스레드를 여러 개 만들어서 일을 동시에 진행”한다. ⭐️⭐️
    • 스레드를 필요 이상으로 너무 많이 늘리게 되면 “메모리 문제가 발생”할 수 있다.
    • 스레드가 많아지게 되면 “경쟁 상태(Race Condition)가 발생”할 수 있음
      • 따라서 경쟁 상태를 해결하기 위해 “특정 데이터에 접근할 수 있는 스레드의 최대 수를 제한하는 Semaphore를 사용”한다.
  • Swift Concurrency (Async / Await) ⇒ “CPU 코어의 수만큼만 스레드를 만들어서 일을 동시에 진행”한다. ⭐️⭐️
    • await 키워드를 통해 비동기 코드를 잠시동안 기다리게 할 수 있다.
    • “기다리는 동안 스레드에 대한 제어권을 시스템에게 넘긴다는 점”이 GCD와 다르다. ⭐️

Task의 특징

  1. Task 블록 자체는 비동기적으로 실행된다. 따라서 Task 블록 내에서 async 함수를 호출할 수 있다.
  2. Task 안에서의 작업은 처음부터 끝까지 순차적으로 진행된다.
  3. Task는 격리되어 있고(isolated), 다른 Task들과 독립적으로 동작한다. ⭐️⭐️⭐️
    • 값이 공유될 상황이 있을 때는 Sendable 프로토콜 체킹을 통해 Task가 격리된 (isolated) 상태로 남아있는지 확인한다. ⭐️⭐️
    • 3번의 특징을 조금 더 풀어서 설명해보겠다. 만약 서로 독립적으로 동작하는 Task들이 서로의 데이터를 공유하게 된다면 어떻게 될까? 예를 들면 아래와 같은 상황이다.
Task {
	let pineApplePicking = Task {
		let pineApples = await harvestPineApples()
		return pineApples.randomElement() // Task가 반환하는 값
	}
		
	// Task가 반환하는 값을 다른 Task에서 기다리는 중이다. 
	let pineApple = await pineApplePicking.value 
}
  • 이 때, pineApple이 “struct와 같은 value 타입”이라면 그냥 복사해주고 서로 갈 길 가면 된다. 한 Task에서 다른 Task로 pineApple 복사본을 전달하고, 각 Task는 각자의 복사본을 챙겨서 떠난다.
  • 복사본을 전달하면 매우 좋은 점이 Task에서 각각 속성값을 변경해도 서로 복사본을 나누어가졌기 때문에 서로의 Task에 아무런 영향도 주지 않는다는 것이다.
  • 그런데 만약 pineApple이 “class와 같은 reference 타입”이라면 메모리 주소값을 서로 공유하게 되기 때문에 Task에서 각각 속성값을 변경하게 되면 서로의 Task에 큰 영향을 줄 수 있게 된다. 이것을 “Data Race” 라고 한다. ⭐️⭐️⭐️ 즉, 공유된 가변 데이터 (Shared Mutable Data) 문제가 발생할 수 있다는 것이다. ⭐️⭐️⭐️

Sendable 프로토콜

  • Sendable 프로토콜이란 Data Race를 생성하지 않고, 서로 다른 격리 도메인 간에 안전하게 공유할 수 있는 타입을 의미한다. 즉, 복사를 통해 값을 동시성 도메인에 안전하게 전달할 수 있는 타입이다.
  • Sendable 프로토콜을 채택하는 조건은 다음과 같다.
    • 값 타입
    • Mutable Storage가 없는 참조 타입
    • 내부적으로 상태에 대한 액세스를 관리하는 참조 타입
    • @Sendable로 표시된 함수 및 클로저

Actor가 생겨난 이유

  • 즉, 1) Data Race를 발생시키지 않으면서도, 2) Task 간에 안전하게 가변 데이터를 공유할 방법이 필요하다. 바로 이 자리에서 Actor가 탄생하였다. ⭐️⭐️
  • Actor는 1) 공유 데이터에 접근해야 하는 여러 Task 를 조정하는 역할을 한다2) 외부로부터 데이터를 격리하고, 3) 한 번에 하나의 Task만 내부 상태를 조작하도록 함으로써 동시 변경으로 인한 Data Race를 피한다. ⭐️⭐️

Actor란?

  • Actor의 목적은 기본적으로 공유되는 가변 상태를 표현하는 것에 있다.
  1. Actor는 class, struct와 마찬가지로 타입이다.
  2. class와 같은 reference type이다.
  3. 상속은 지원하지 않는다.
  4. Sendable 프로토콜을 지원한다. ⭐️⭐️

Actor의 특징 ⭐️⭐️

  • Actor의 특징을 설명하기에 앞서, Data Race가 왜 생겨나는지부터 곰곰이 생각해보자.
    • Data Race가 발생하는 이유두 개 이상의 스레드가 “서로 공유하고 있는 데이터에 접근”하여 “데이터를 수정하고자 할 때 발생”한다. 즉, 데이터에 접근하되 "데이터를 수정하지 않고 읽기만 한다면” 아무런 문제가 발생하지 않는다. ⭐️⭐️ 그렇다면 공유 데이터에 한 번에 하나씩만 접근하게 한다면? 결과적으로 Data Race를 막을 수 있지 않은가?
    • 이렇게만 한다면 Data Race가 발생할 일은 없다. 그리고 Actor가 바로 이 특징을 가지고 있다! Actor에서는 동시에 하나의 Task만 접근할 수 있도록 되어 있다. 즉, Actor 내에서 사용되는 속성은 여러 스레드에서 동시에 접근할 수 없다. ⭐️⭐️
  • Actor는 Data Race를 피하기 위해 “잠시동안 호출코드를 기다리게 할 수 있다.” 이를 위해 await 코드를 사용한다. ⭐️⭐️
  • Actor 외부에서 Actor에 있는 “변수” 나 “메소드”에 접근한다면 await이 반드시 필요하다. ⭐️⭐️
  • 또한 Actor는 가장 우선순위가 높은 작업을 먼저 실행한다. (Actor Reentrancy)
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)
	// 1. 상수는 변경 불가능하기 때문에 어느 스레드에서 접근해도 안전함. 
	// actor 외부에서도 바로 접근이 가능하다.
    let name = wallet.name 

	// 2. 반면 actor 외부에서 변수에 접근한다면 await이 꼭 필요하다.
    let amount = await wallet.amount 

	// 3. actor 외부에서 메서드를 호출 할 때에도 await가 필요하다.
    await wallet.spendMoney(ammount: 100) 

	// 4. actor 외부에서 actor 내부의 "변수"를 변경할 수 없음
	// 즉, await을 사용해도 외부에서 actor 내부의 "변수"를 변경할 수 없음 ⭐️⭐️
    await wallet.amount += 100 
}

Actor Reentrancy ⭐️⭐️

  • 만약에 Actor 자체에서 await을 통해 Actor의 실행을 중지하는 경우에는, 다른 Task에서 Actor로 진입해 코드를 실행할 수 있다. 이것을 바로 “Actor Reentrancy” 라고 한다. ⭐️⭐️

  • 조금 더 쉽게 예를 들어보겠다.

    • 예를 들어, 어떤 앱에 Database Actor, Sports feed Actor, Weather feed Actor, Health feed Actor가 존재한다고 가정해보자.
    • Database Actor가 스레드를 잡고 실행 중에, await 키워드를 사용해서 잠시 멈추었다고 가정해보자. 이 상태에서는 다른 Actor들이 Database Actor의 프로퍼티나 메소드에 접근할 수 있다는 뜻이다.
    • 그래서 Sports feed Actor가 Database Actor에 접근해 save() 메소드를 호출했다.
    • 이것이 Actor Reentrancy가 의미하는 바이다. “우선순위에 따라서” Actor는 Actor가 중단되어 있는 동안 “더 중요한 부분을 먼저 실행” 할 수 있다. ⭐️⭐️

혹시 잘못된 부분이 있다면 댓글로 피드백 주시면 감사하겠습니다!

출처

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

profile
꽃말 == 반드시 오고야 말 행복

4개의 댓글

comment-user-thumbnail
2024년 3월 1일

Actor에서 await을 썼다면 이후 상태가 어떻다고 섣불리 단정하지 말라고 하죠
await된 사이에 어떤 일이 일어날 지 모르니까요
마치 냉동인간이 해동 됐더니 세상이 변해버렸다는 이야기처럼요

라인 엔지니어링 블로그에서 GCD랑 Concurrecy를 일종의 부하테스트를 통해 성능을 비교한 게 있는 데 관심 있으시면 읽어보시는 것도 좋을 것 같습니다

1개의 답글
comment-user-thumbnail
2024년 3월 2일

아직 익숙하지 않은 키워드 Actor에 대해 정리해주셔서 감사드려요!
복잡하고 중요한 요소들이 많았던 만큼, 볼드한 문구들이 가독성을 조금 아쉽게 하는 것 같아요.
물론 제 부족한 이해력이 너무 크기에...🥲 꾸준히 글로 정리해야겠네요!!

안그래도 async await 관련해서 내용 정리해보면서 차근차근 이해해보고 있었는데...
참고해보면서 actor에 대해서도 많이 배워보겠습니다~

1개의 답글