nyancoder

Swift의 Actor와 구현 가볍게 살펴보기 본문

Swift

Swift의 Actor와 구현 가볍게 살펴보기

nyancoder 2021. 7. 25. 02:34

새 Swift버전에 Actor가 추가되면서 여러 스레드에서 접근했을 때 발생할 수 있는 여러 동시성 문제들을 해결하는 좋은 해결책 중 하나로 관심을 받고 있습니다.

기존에도 동시성 문제를 해결하기 위해서 각종 Lock이나 DispatchQueue 등을 활용하여 문제를 해결할 수 있었지만, Actor를 사용하면 컴파일러가 근본적으로 동시성 문제를 확인하고 오류를 일으킬 수 있습니다.

 

Actor의 역할은 크게 세 가지로 볼 수 있습니다.

  • 코드 생성 단계 - actor내에 선언된 함수에 async를 자동으로 적용해주고, actor의 구현에 필요한 코드를 자동으로 생성해 줍니다.
  • 컴파일 단계 - 동시성 문제가 발생할 수 있는 코드를 작성하면 컴파일러가 오류를 일으켜서 수정하도록 해 줍니다.
  • 실행 단계 - 각 코드가 실제로 순서대로 실행될 수 있게 해 줍니다.

 

이런 Actor의 구현은 어떻게 이루어져 있는지를 살펴보도록 하겠습니다.

 

우선 아래의 코드를 보면 actor는 Actor프로토콜을 상속받게 해주는 기능임을 알 수 있습니다.

actor TestActor {
    var property: Int = 0
}

let testActor = TestActor()
print("\(testActor is Actor )") // true

 

여기서의 Actor 프로토콜의 정의는 XCode에서 확인할 수 있습니다.

/// Common protocol to which all actors conform.
///
/// The `Actor` protocol generalizes over all actor types. Actor types
/// implicitly conform to this protocol.
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
public protocol Actor : AnyObject, Sendable {

    /// Retrieve the executor for this actor as an optimized, unowned
    /// reference.
    ///
    /// This property must always evaluate to the same executor for a
    /// given actor instance, and holding on to the actor must keep the
    /// executor alive.
    ///
    /// This property will be implicitly accessed when work needs to be
    /// scheduled onto this actor.  These accesses may be merged,
    /// eliminated, and rearranged with other work, and they may even
    /// be introduced when not strictly required.  Visible side effects
    /// are therefore strongly discouraged within this property.
    nonisolated var unownedExecutor: UnownedSerialExecutor { get }
}

이 코드를 보면 Actor는 class의 일종이며, 동시에 Sendable 프로토콜을 만족해야 합니다.

또한 unownedExecutor를 가지고 있는데, 주석을 통해서 actor에 전달된 값이 이 executor를 통해서 실행된다는 것을 알 수 있습니다.

 

Sendable

actor의 첫 번째 역할인 동시성 문제에 대해서 컴파일러의 오류를 일으키는 역할을 담당하는 프로토콜입니다.

 

Sendable프로토콜을 만족시킨다는 것은 서로 다른 스레드 간에서 공유되어도 문제가 발생하지 않는다는 것을 의미합니다.

대표적인 예가 숫자와 같은 값, 불변 객체, 내부적인 동기화 알고리즘을 가진 객체 등이 있습니다.

따라서 Actor protocol은 동기화 처리가 되어있으므로 Sendable을 만족하며, 이는 actor자체를 다른 actor 간에도 공유할 수 있게 만들어 주기도 합니다.

 

Executor

actor가 가지고 있는 UnownedSerialExecutor는 성능을 위해 unowned 하고 있을 뿐인 SericalExecutor라고 볼 수 있습니다.

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
@frozen public struct UnownedSerialExecutor {

    @inlinable public init<E>(ordinary executor: E) where E : SerialExecutor
}

그리고 SerialExecutor는 이름대로 들어온 요청을 순서대로 실행하는 기능을 수행합니다.

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
public protocol Executor : AnyObject, Sendable {

    func enqueue(_ job: UnownedJob)
}

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
public protocol SerialExecutor : Executor {

    func enqueue(_ job: UnownedJob)

    func asUnownedSerialExecutor() -> UnownedSerialExecutor
}

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
@frozen public struct UnownedJob {
    public func run()
}

따라서 actor의 함수를 호출하면 이 함수의 실행이 UnownedJob으로 변경되어 SerialExecutor로 전달된 다음 실행되는 것을 추측할 수 있습니다.

 

final class CustomSerialExecutor: SerialExecutor {
    static let instance = CustomSerialExecutor()

    func enqueue(_ job: UnownedJob) {
        print("enqueue")

        DispatchQueue.main.async {
            job.run()
        }
    }

    func asUnownedSerialExecutor() -> UnownedSerialExecutor {
        return UnownedSerialExecutor(ordinary: self)
    }
}

actor TestActor {
    var property: Int = 0
    
    nonisolated var unownedExecutor: UnownedSerialExecutor {
        return CustomSerialExecutor.instance.asUnownedSerialExecutor()
    }
    
    func run() {
        property = property + 1
    }
}

let testActor = TestActor()
Task {
    await testActor.run()
}

// output: enqueue

이와 같이 actor의 run을 호출하면 actor에서 executor를 얻어와 job을 추가해서 실행하는 것을 알 수 있습니다.

 

따라서 actor에서 동시성 문제를 처리해주는 부분은 executor이며, 이 executor가 들어오는 작업들을 순서대로 처리해주는 것을 알 수 있습니다.

 

또한 기본 actor와 Main Actor의 경우는 실행되는 스레드가 다른데 이는 아래와 같음을 추측해볼 수 있습니다.

  • 기본 actor - 기본 Executor가 스레드 풀을 사용하여 작업을 수행할 것입니다.
  • MainActor - Executor가 DispatchQueue.main.async를 사용해서 작업을 수행할 것입니다.

 

Summary

지금까지 알아본 것을 정리하면 아래와 같습니다.

  • actor키워드를 사용하면 컴파일러가 클래스를 만들고 Actor프로토콜을 상속받게 처리해줍니다.
  • actor는 Sendable 속성을 가지고 있어 안정적으로 스레드 간에 전달이 가능하고, 이를 위반하는 로직은 컴파일 오류가 발생합니다.
  • actor는 자신이 참조하는 executor를 통해서 실행할 스레드를 결정하고 각 호출이 동시성 문제없이 순차적으로 처리되도록 합니다.

'Swift' 카테고리의 다른 글

Property Wrapper를 이용하여 파라메터의 속성 제한하기  (0) 2021.08.01
Comments