Actor

2025. 5. 8. 21:08·iOS/Swift Concurrency
반응형

👋 들어가기 전

먼저 간단한 선행 내용을 먼저 정리한 이후 엘런님 강의를 듣고 나서 다시 한번 더 내용을 채우러 오겠다.

🏁 학습할 내용

  • Actor란 무엇인가
  • 특징 및 동작방식
  • 어떻게 동기화 메커니즘이 가능하지?
  • 격리와 비격리
  • Actor 재진입(Reentrancy)
  • Actor Reprioritization
  • Actor Hopping

🕴Actor란 무엇인가

먼저 여기서 Actor에 관한 내용이 비대해질 경우 별도의 포스팅으로 빼도록 하겠다.

현재는 간단하게만 알아보자.

🌁 등장 배경

먼저 왜 등장하게 되었는지 알아보자.

Actor는 WWDC21에서 소개됐다.

🧵 동시성 프로그래밍

개인적인 생각으로는 서비스가 커지면, 퍼포먼스 향상, 즉 최적화에 대한 열망이 높아지기 마련이다.

여기에 큰 영향력을 주는 것이 동시성 프로그래밍인 것 같다.

 

동시성 프래그래밍은 "많은 작업을 여러 개의 스레드에 분산시켜 성능을 개선하는 기법"

이라고 간단히 정리할 수 있다.

 

성능 개선이 된다면 장점밖에 없을까/?

 

그렇지 않다. 성능이 개선되는 만큼 신경써야하는 중요한 부분이 생긴다. 

바로 Data-Race이라는 다소 복잡한 개념이 등장한다.

 

Data-Race를 인형 뽑기로 비유하면 이해하기가 조금 쉽다.

 

집게가 1개일 때는 인형을 갖기 위해 경쟁 상태는 발생하지 않는다.

 

하지만 집게가 여러 개 있다고 생각해 보자.

인형 입장에서는 어떤 집게에 뽑힐지 전혀 알 수 없는 입장이다.

 

동시성 프로그래밍은 여러 개의 스레드로 작업을 분산하는 것이기 때문에 단일 스레드처럼

코드를 써 내려가면 안 된다.

 

반드시 Data-Race를 막는 작업을 해줘야 한다.

🧱 concurrency가 없을 때 어떻게 막았을까?

나의 경험으로는 크게 2가지 방법을 많이 썼던 것 같다.

 

1. 직렬큐

  • 한 줄로 순서대로 접근하게하여 충돌을 방지하는 메커니즘

2. Lock

  • 잠금 메커니즘을 통해 이미 사용중일 경우 접근을 막는 메커니즘
  • 잠금/해제 과정을 실수할 경우 데드락이 발생할 수 있음

다양한 방법론이 있지만 위 2가지 방법을 많이 쓴 것 같다.

이 때 두 방법론의 공통적인 불편한 점은 Data-Race 관리 책임이 개발자에게 존재한다.

🦸 Actor 등장

 

WWDC 영상을 참고하면 Actor는 다음과 같이 정의된다.

 

1. Actors provide synchronization for shared mutable state

  • 액터는 여러 곳에서 동시에 접근할 수 있는 변경 가능한 상태(shared mutable state)에 대해 동기화 메커니즘을 제공한다.
  • 액터는 여러 스레드에서 공유되는 데이터를 안전하게 보호해주는 구조입니다.

2. Actors isolate their state from the rest of the program

  • 액터는 자신의 상태를 프로그램의 다른 부분으로부터 격리한다.
  • 액터 내부의 값은 외부에서 직접 접근할 수 없고, 액터를 통해서만 접근할 수 있게 제한되어 있다.

3. All access to that state goes through the actor

  • 상태에 대한 모든 접근은 반드시 액터를 거쳐야 한다.
  • 외부에서 값을 읽거나 바꾸려면 액터에 메시지를 보내야 하며, 액터가 내부에서 처리하게 된다.

조금 더 쉽게 풀어서 해석하기 위해 엑터 역시 비유로 설명을 해보자.

 

엑터는 인형뽑기 가게 주인, 스레드와 데이터는  마찬가지로 집게와 인형이라고 예를 들어보자.

 

이때 문제점을 다시 정리하면 인형에 한번에 많은 집게가 접근해서 문제가 됐고

가게주인은 이제 인형에 접근하기 전에, 무조건 자신을 통과해서 접근하게 관리한다.


✨ 특징

Actor의 대표적인 특징은 다음과 같다.

  • 참조 타입
  • 프로퍼티, 메서드 선언 가능
  • 프로토콜, 확장 가능
  • 상속 불가능, 그러므로 상속과 관련된 super, final, open, class, override 등의 키워드는 사용불가

⚙️ 동작 방식

동작방식을 코드와 함께 그림으로 살펴보자.

먼저 이전에 사용했던 개념인 엑터 = 가게 주인, 스레드 = 집게, 데이터 = 인형이라는 비유를 그대로 대입해보자.

 

먼저 actor안에 있는 변수에 접근해보자.

actor Owner {
  var doll: String = ""
}

let actor = Owner()

print(actor.doll)

 

외부에서 접근하려는데 async 키워드가 붙어있다 ??

❓ 왜 async가 붙었을까??

Actor는 내부적으로 동기화 과정이 있기때문에 내부 저장 프로퍼티를 외부에서 접근할 때
반드시 하나의 스레드만 접근할 수 있다.

 

그렇기 때문에 호출하는 외부 입장에서는 나의 컨텍스트가 언제 actor 내부 진입을 할 지 알수 없다.

 

그렇기 때문에 actor외부에서 내부를 접근 시, 잠재적 중단 포인트 (await)가 필요하다.


🤔 어떻게 동기화 메커니즘이 가능하지?  (추가)

 

아주 중요한 부분을 오늘 (25.05.19) 컨커런시 선생님이 꼬집어 주셨다.

actor라는 개념이 swift만의 개념이 아니라 동시성 프로그래밍에서 전반적으로 사용되는 패러다임이다.

 

https://actoromicon.rs/ch03-00-actors.html

위 링크는 rust 언어의 actor 관련 문서인데

 

여기서 흥미로운 내용이 있다.

 

📪 Actor는 자신만의 mailbox를 갖는다.

 

⭐️ MailBox의 역할

MailBox는 메시지를 받는 큐 같은 느낌이다.

 

외부로 부터 Actor 내부로 진입할 때, MailBox에 진입에 대한 메시지가 쌓인다.

이후 Actor는 MailBox에 쌓여있는 메시지를 하나씩 꺼내 직렬 실행자(Serial Executor)에게 전달한다.

 

즉, Mailbox구조가 actor 내부에 접근할 때, 단일 스레드만 허용하게 해주는 이유이다.

 

이때 중요한점은 Swift 컨커런시는 먼저 들어온 것이 먼저 실행되는(FIFO) 구조가 아닌,
우선순위가 높은 것을 최대한 먼저 실행하도록 구현되었다.

 

여기서 말한 우선순위는 Task 우선순위가 아니다.

 

바로 suspand된 작업이 우선순위를 의미한다.

 

 

한번 정리해보자.

  • Mailbox에 먼저 접근한 순서로 메시지가 쌓인다.
  • 이후 직렬 실행자가, Mailbox에 쌓여있는 메시지를 하나씩 꺼낸다. (FIFO)
  • 작업을 처리하지만, 완료는 보장하지 않는다.
  • 작업은 끝날수도 있고, 잠시 중단될 수 있다.
  • 중단 될 경우, 높은 우선순위를 부여받아 suspand된다.
  • 이후 다음 메시지를 Mailbox에서 꺼내와 작업하고, 작업이 종료되면, Mailbox에서 메시지를 꺼내지 않고, suspand된 작업을 최우선으로 처리한다.


🔐 격리와 비격리

 

동작 방식에서 살펴봤던 내용을 조금 더 자세하게 상황을 분리해서 살펴보자.

 

🔒 격리

 

먼저 격리, 즉 Actor의 동기화 메커니즘이 반드시 필요한 접근 방식은 다음과 같다.

  • mutable 변수를 외부에서 접근
  • mutable 변수를 값을 변경시키는 메서드

코드와 함께 살펴보자.

 

actor 내부에 접근하는 곳은 Task 도메인에서 접근하고 있다.

 

이곳은 actor 기준 외부이기 때문에 내부 격리 영향을 받는다.

그러므로 await 키워드가 붙는다.

 

이번에는 내부 add 함수를 봐보자.

내부에서는 mutableVar을 수정하는데도 await 키워드가 붙지 않는다.

 

왜냐하면 actor 내부에서는 내부 격리 환경이 기본적으로 적용되어 있기 때문에
Data-Race가 발생하지 않는다. (default isolated)

 

정리하면 actor 내부 격리는 다음과 같은 특징이 있다.

  • 내부에서는 기본적으로 isolated 되어 있기때문에 isolated 키워드가 필요없다.
  • 그렇기 때문에 내부 변수가 수정할 때는 반드시 내부 메서드를 통해 수정되야한다.

그렇다면 isolated 키워드는 언제 쓸가?

 

🎯 격리 주체 정하기

 

actor를 함수 파라미터로 전달할 때 isolated 키워드를 통해 격리를 담당할 actor임을 알려주면

외부에서 접근하는 형태이지만 사실, 내부에서 접근하는 것처럼 동작할 수 있다.

 

즉 async/ await을 쓸 필요가 없다.

 

아래 사진은 2023년 애플 아카데미에서 컨커런시 관련 발표를 할 때 사용한 ppt이며

간단한 코드도 함께 보면 조금 더 이해가 쉬울 것이다.

actor Actor {

  var mutableVar: Int = 1

  let constant: Int = 2

  nonisolated func readSync() -> Int {
    return constant
  }

  nonisolated func readAsync()  async -> Int {
    return await mutableVar
  }

}

var actor = Actor()

//noniolated
func print1() async {
  print(await actor.mutableVar)
}

// isolated
func print2(actor: isolated Actor) {
  print(actor.mutableVar)
}

🔑  비격리 (nonisolated)

그러면 actor 내부 접근 시 항상 격리가 필요할까?

actor의 등장 배경이 뭐였었지??

 

바로 Data-Race 해결을 위해서 등장했다.

반대로 말하면 Data-Race가 필요 없다면, actor의 격리가 필요없다.

 

이 때 사용하는 것이 nonisolated 키워드이며, 다음과 같은 형태로 사용된다.

  • let(immutable)
  • read-only-function
  • 격리가 필요하지 않을 때 (동기, static등 ..)

 

여기서 특이한 점은, 분명 actor 내부는 동기화가 기본이라 await가 필요없다고 했는데

readAsync 함수를 보면 await가 붙어있다!

 

nonisolated 키워드가 붙어있기 때문에 actor내부 격리 구간이 아니다.

그렇기 때문에 외부에서 접근하는 것과 같은 방식으로 await를 통해 차례를 기다려야한다.


♻️ Actor 재진입(Reentrancy)

Actor안에 함수를 호출하는데 이때, 내부에 비동기 함수가 있으면 어떻게 될까??

 

먼저 WWDC 영상에 있는 코드와 함께 살펴보자.

 

1️⃣ Task1

ImageDownloader 엑터안의 image함수를 Task1이 호출했다.

이때 image함수 안에는 이미지를 다운로드 하는 비동기 함수가 있다.

 

Actor는 내부 동기화가 진행되어, Task1이 cached 가변 변수에 접근했기 때문에, 다른 Task에 대해

접근을 기다리게했다.

 

하지만 Task1이 download함수를 만나 suspand 되었기 때문에 Task2가 접근할 수 있다.

 

2️⃣  Task2

Task2 역시 download 함수로 인해 suspand에 들어갔고, 이후 Task1이 끝나고 cach에 값이 채워진다.

이때, Task2가 끝나게 되면, 이미 Task1으로 값이 채워졌는데, 값이 다시 써지는 Data-Race가 발생하게된다.

 

그러면 여기서 이런 질문을 할 수 있다

아니 Data-Race를 막는게 actor인데 왜 Data-Race가 발생하지??

suspension point는 그 지점에서 스레드 제어권을 시스템에게 넘기는 지점이다.

즉 해당 스레드는 다른 작업하러 떠남

 

🅰️ Actor는 잘못한거 없음

actor 입장은 이렇다

나에게 접근한 스레드가 다른 일 하러 떠났네?? 다음 작업(스레드) 들어와

 

이때 Task2가 진입해 다시 다운로드를 진행하고, 이전에 발생한 다운로드(Task1) 작업이 끝나

다시 다운로드 아래 코드(재진입)를 진행하게된다.

 

여기서 굉장히 큰 주의사항이 있는데.

 

재진입에 할당된 스레드 != 중단 전 할당된 스레드

 

물론 운이 좋게 같을 수는 있지만, 같다고 생각하고 코드를 짜면안된다!

절대 보장되지 않는 경우이다.

✅ 그러면 Actor 함수 내 Data-Race는 어떻게 해결해?

1. 딕셔너리 default값 이용하기

 

문제가 됐던 비동기 코드 아래에서, 결과를 저장할때, default 값으로 넘겨주게되면

이미 cache 딕셔너리에 해당 값이 있으면 기존값, 없으면 결과값을 저장하게 되어

깔끔이 해결할 수 있다.

2. State 관리

이 방법은 아래 참고된 유튜버분 영상에 찾았는데, 개인적으로 이 방법이 더 직관적인 것 같다.

  • 현재 key값이 inProgress 일때는, 그 task의 결과를 리턴
  • 이미 completed 된 값이 있다면, 결과 리턴
  • 아직 없면 비동기 함수 실행 및 담당 Task 생성

  • 생서된 Task를 key를 통해 cache에 .inProgress State로 저장
  • 이후 결과 도착하면 , .complated 상태로  result를 저장 후 리턴

2번 방법을 쓰면 상태에 따른 디버깅이 훨씬 쉬울 것 같다.


🏆 Actor의 우선순위

🚂 GCD에서 우선순위 역전

이전 GCD를 사용할 때 우선순위를 담음과 같이 설정할 수 있었다.

또는 Queue 자체에 우선순위를 줄 수 있었다.

 

앞에 우선순위가 낮은게 작업이있고, 뒤에 우선순위가 높은게 작업이 있으면 GCD는 내부적으로
앞에 있는 앞 작업들의 우선순위를 강제로 높혀 처리한다.

 

즉 선입 선출의 형태를 가져가기 위해, 앞의 우선순위를 강제로 올리는 오버헤드가 있다.

 

 

✅ Actor Reprioritization

Actor는 뒤 Task가 우선순위가 높아도 먼저 실행될 수 있게 한다.

⚠️  여기서 먼저 실행될 수 있다는 것이지, 무조건 먼저 실행된다라는 뜻은 아니다.
import Foundation
func swiftConcurrency() async {
    for i in 0 ..< 5 {
        Task(priority: .low) {
            print("low Start\(i)")
            _ = try? await URLSession.shared.data(from: URL(string: "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/41/19/5c/41195c0e-da2f-de45-6924-325e2fab279d/mzl.dgcmevrw.png/576x768bb.png")!)
            print("low End\(i)")
        }
    }

  Task(priority:  .medium) {
      print("medium Start")
      _ = try? await URLSession.shared.data(from: URL(string: "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/41/19/5c/41195c0e-da2f-de45-6924-325e2fab279d/mzl.dgcmevrw.png/576x768bb.png")!)
      print("medium End")
  }

  Task(priority: .high) {
        print("high Start")
        _ = try? await URLSession.shared.data(from: URL(string: "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/41/19/5c/41195c0e-da2f-de45-6924-325e2fab279d/mzl.dgcmevrw.png/576x768bb.png")!)
        print("high End")
    }
}

Task {
    await swiftConcurrency()
}

//OUTPUT

/*
low Start0
low Start1
high Start
low Start2
medium Start
low Start4
low Start3
low End1
medium End
low End3
low End2
high End
low End4
low End0
*/

 

시작 지점을 보면 high와 medium이 low4와 low3보다 먼저 실행에 들어갔다.

 

이게 가능한 이유는 앞에서 배운 Actor 재진입 덕분에 FIFO 방식으로 일을 처리하지 않기 때문이다.


🪜Actor Hopping

 


우리는 이전까지, Actor 내부에서 동작과, Task에서 엑터의 메서드를 사용할 때의 흐름을 알아봤다.

그런데 Actor 끼리도 서로 await/ async 호출을 통해 작업을 호출할 수 있다.

 

이 떄 Actor는 자신만의 격리 구간이 있는데 다른 Actor의 메서드를 호출함으로서 격리 구간이

다른 Actor로 전환된다.

 

⭐️ 정의

Actor의 작업은 협력 쓰레드 풀(cooperative thread pool)에서 수행되는데
현재 actor에서 다른 actor로 작업의 전환되는 걸 Actor Hopping이라고 한다.

 

✨ 특징

  • actor hopping 시 thread는 block 되지 않는다.
  • cooperative thread pool에서 수행되는 actor간의 hopping은 context switch 비용이 없다.
  • 잦은 hopping은 context switch 오버헤드가 발생할 수 있다.

 

🚧 MainActor와의 hopping

MainThread는 Cooperative thread pool과 분리되어 있기 때문에

MainActor와 다른 actor간의 hopping은 context switch 비용이 발생하기 때문에

 

conetext switch 횟수를 최대한 줄여야 효율성이 올라간다.


😀 소감 및 마무리

회사에서 컨커런시 관련 내용이 많아져서 애플 아카데미에서 했던 컨커린시 발표자료와

함께 한번 더 블로그애 정리해보니 이제 어떤 느낌인지 감이 많이 잡힌 것 같다.


출처

https://github.com/swiftlang/swift-evolution/blob/main/proposals/0306-actors.md

 

swift-evolution/proposals/0306-actors.md at main · swiftlang/swift-evolution

This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution

github.com

 

https://developer.apple.com/videos/play/wwdc2021/10133/

 

Protect mutable state with Swift actors - WWDC21 - Videos - Apple Developer

Data races occur when two separate threads concurrently access the same mutable state. They are trivial to construct, but are notoriously...

developer.apple.com

 

https://developer.apple.com/documentation/swift/actor

 

Actor | Apple Developer Documentation

Common protocol to which all actors conform.

developer.apple.com

https://github.com/DKS-wift/SwiftConcurrency

 

GitHub - DKS-wift/SwiftConcurrency: SwiftConcurrency 관련 정리 레포 (2023.07.01 ~ 07.28)

SwiftConcurrency 관련 정리 레포 (2023.07.01 ~ 07.28). Contribute to DKS-wift/SwiftConcurrency development by creating an account on GitHub.

github.com

https://www.youtube.com/watch?v=NSMvwjO7Ono

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

 

[Swift] Actor 뿌시기

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

sujinnaljin.medium.com

 

반응형

'iOS > Swift Concurrency' 카테고리의 다른 글

@Sendable  (0) 2025.05.11
@MainActor  (0) 2025.05.08
컨커런시 문법 정리  (0) 2025.03.22
@TaskLocal  (1) 2025.03.22
Task Cancellation  (0) 2024.10.27
'iOS/Swift Concurrency' 카테고리의 다른 글
  • @Sendable
  • @MainActor
  • 컨커런시 문법 정리
  • @TaskLocal
Hamp
Hamp
남들에게 보여주기 부끄러운 잡다한 글을 적어 나가는 자칭 기술 블로그입니다.
  • Hamp
    Hamp의 분리수거함
    Hamp
  • 전체
    오늘
    어제
    • 분류 전체보기 (335) N
      • CS (30)
        • 객체지향 (2)
        • Network (7)
        • OS (6)
        • 자료구조 (1)
        • LiveStreaming (3)
        • 이미지 (1)
        • 잡다한 질문 정리 (0)
        • Hardware (2)
        • 이론 (6)
        • 컴퓨터 그래픽스 (0)
      • Firebase (3)
      • Programing Langauge (41)
        • swift (34)
        • python (6)
        • Kotlin (1)
      • iOS (134)
        • UIKit (37)
        • Combine (1)
        • SwiftUI (34)
        • Framework (7)
        • Swift Concurrency (22)
        • Tuist (6)
        • Setting (11)
        • Modularization (1)
        • Instruments (6)
      • PS (59)
        • 프로그래머스 (24)
        • 백준 (13)
        • LeetCode (19)
        • 알고리즘 (3)
      • Git (18)
        • 명령어 (4)
        • 이론 (2)
        • hooks (1)
        • config (2)
        • action (7)
      • Shell Script (2)
      • Linux (6)
        • 명령어 (5)
      • Spring (21)
        • 어노테이션 (6)
        • 튜토리얼 (14)
      • CI-CD (4)
      • Android (0)
        • Jetpack Compose (0)
      • AI (17) N
        • 이론 (10)
        • MCP (1)
        • LangGraph (6) N
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    dp
    투포인터
    Swift
    dispatch
    GIT
    CS
    AVFoundation
    lifecycle
    IOS
    boostcamp
    Spring
    dfs
    protocol
    백준
    Tuist
    프로그래머스
    SwiftUI
    property
    concurrency
    UIKit
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.0
Hamp
Actor
상단으로

티스토리툴바