ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Actor in Swift concurrency
    iOS 2022. 12. 31. 07:39

     

     Actor는 Swift concurrency의 핵심으로, async/await 및 structured concurrency와 함께 작동하여 정확하고 효율적인 동시성 프로그래밍 구축을 돕습니다.

     

     데이터 경합(Data Race)은 두 개의 별도의 스레드가 동일한 데이터에 동시에 접근하고 이러한 접근 중 적어도 하나가 '쓰기' 작업일 때 발생합니다. 데이터 경합이 발생할 수 있다고 판단되면, actor를 사용해서 low-level에서 데이터 경합을 방지할 수 있습니다.

     

     데이터 경합은 공유되는 가변 상태로 인해 발생합니다. struct는 동시에 접근할 수 있는 코드가 있을 때 컴파일하는 것을 허용하지 않으며, 값 타입이므로 따로 변수에 복사해서 할당한 후 작업을 수행하면 데이터 경합은 발생하지 않지만, 그것은 별개의 값이므로 공유의 의미가 없습니다. 따라서 변수의 값 증가에 더불어 추가적인 작업을 하는 참조 타입(이 글에서는 class, actor)을 예로 들어보겠습니다.

     

     


     

    Class

    class CounterClass {
        var value = 0
        
        // 증가 후 반환
        func increment() -> Int {
            value += 1
            return value
        }
    }

     

     

     


     

    class 에서 increment()만 중복으로 실행한다면?

    // run
    let counter = CounterClass()
    
    Task.detached {
        print(counter.increment())
    }
    
    Task.detached {
        print(counter.increment())
    }

     

     

    실행 결과

    // 예상 가능
    1
    2
    
    // 예상 가능
    2
    1
    
    // 두 Task 모두 각각 0을 읽고(read) 1 증가(write)
    1
    1
    
    // 두 Task 모두 증가 작업 후에 반환
    2
    2

     실제로 위처럼 단순하게 구현하고 실행해봤을 때는 1, 2 또는 2, 1을 반환했습니다. 하지만 데이터 경합이 발생할 수 있으므로 실행 시점에 따라 어떻게 나올지 모릅니다. 이는 경쟁을 유발하는 데이터 접근이 곳곳에 있을 수 있으며 운영 체제의 스케줄러가 프로그램을 실행할 때마다 다른 방식으로 동시 작업을 수행할 수 있으므로 확실한 결과를 보장받지 못하기 때문입니다. 하지만 이를 방치한 상태로 규모를 키우면서 작업하면, 문제가 발생했을 때 디버깅에 어려움을 겪겠죠?

     

     


     

    데이터 경합을 눈으로 확인하기 위해 추가적인 작업을 수행

    단순하고 명확한 결과를 보기 위해 조금 불필요한 작업을 추가해봤습니다.

    class CounterClass {
        var value = 0
        
        // "wait"를 세 차례 출력하고 반환
        func incrementWithWait() -> Int {
            value += 1
            for _ in 0...2 {
                print("wait")
            }
            return value
        }
        
        // 증가 후 반환
        func increment() -> Int {
            value += 1
            return value
        }
    }

     

     

     

    실행

    // run
    let counter = CounterClass()
    
    Task.detached {
        print(counter.incrementWithWait())
    }
    
    Task.detached {
        print(counter.increment())
    }

     

     

    실행 결과

    // 여러가지 경우 중 하나
    wait
    wait
    wait
    1
    2
    
    // incrementWithWait()에서 증가시키고 출력하는 동안, increment()에서 1 증가시키고 반환
    // 이후, 출력을 마치고 반환
    2
    wait
    wait
    wait
    2
    
    ...

    실행 결과는 위와 같이 다양하게 나옵니다. 결과를 예상할 수 없습니다. 상황에 따라 문제가 발생할 수도, 아닐 수도 있습니다. 이를 actor를 사용하여 데이터 경합을 제거함으로써 결과를 보장받을 수 있습니다.

     


    Actor

    // class -> actor
    actor CounterActor {
        var value = 0
        
        func increment() -> Int {
            value += 1
            return value
        }
        
        func incrementWithWait() -> Int {
            value += 1
            for _ in 0...2 {
                print("wait")
            }
            return value
        }
    }
    
    let counter = CounterActor()
    
    Task.detached {
        // 일시 중단할 수 있음을 알림.
        // 다른 작업을 먼저 수행하게 된다면, actor가 자유로워질 때까지 기다려야 함.
        print(await counter.incrementWithWait())
    }
    Task.detached {
        // 일시 중단할 수 있음을 알림.
        print(await counter.increment())
    }

     

    실행 결과

    // actor의 동기 코드는 항상 중단없이 완료될 때까지 실행됨.
    // incrementWithWait()를 수행하는 Task 이후, increment()를 수행하는 Task가 발생한 경우.
    wait
    wait
    wait
    1
    2
    
    // 상황에 따라서 increment()가 먼저 실행되면서 이렇게도 출력될 수 있을 것 같습니다.
    1
    wait
    wait
    wait
    2

     

    1, 1 또는 2, 2 의 결과는 나올 수 없습니다. 어떻게 결과를 보장받을 수 있는 걸까요? actor는 자체의 상태를 가지며 해당 상태는 프로그램의 나머지 부분과 격리(isolate)됩니다. serial dispatch queue를 사용함으로써 얻는 것과 동일한 상호 배제를 제공합니다. 이를 위한 메커니즘이 있습니다.

    • 외부에서 actor와 상호작용할 때, 다른 코드가 actor의 상태에 동시에 접근하지 않도록 보장.
    • actor가 다시 자유로워지면 기다리고 있던 실행(접근)을 재개.
    • await 키워드는 액터에 대한 비동기 호출이 위와 같은 일시 중단을 포함할 수 있음을 나타내는 것.

     


     

    Actor isolation

     

     데이터 경합은 동시에 동시에 접근하고, 접근 중 적어도 하나의 작업이 쓰기일 때 발생한다고 했죠? 그렇다면, 해당하지 않는 작업을 하는 동안에는 actor로의 접근을 막을 필요가 없습니다. 그렇게 한다면 처리만 늦어질 테니까요. 이럴 때, nonisolated 키워드를 사용해서 Actor-isolation에서 분리된 작업으로 처리해야 합니다. actor에 격리되지 않았다는 의미로 해석할 수 있을 것 같습니다. 다음과 같이 또다른 작업을 추가해보겠습니다.

    actor CounterActor {
        var value = 0
        
        func increment() -> Int {
        	// 공유 가변 데이터에 접근해서 증가시킴.
            value += 1
            
            // 이후에는 접근할 필요가 없다면..
            let convetedValue = doSomething(value)
            
            return convertedValue
        }
        
        // nonisolated
        nonisolated func convert(_ value: Int) -> String {
        	// 증가시킨 value를 가지고 가공을 하는 등 작업을 수행
            ...
        }
    }

     nonisolated는 구문적으로 actor에 설명되어 있지만 actor 외부에 있는 것으로 취급됨을 의미합니다. 예를 들어, nonisolated 함수는 변경할 수 없는 let 프로퍼티를 참조하여 처리하는 작업이 가능하며, 그런 작업을 하고 있을 때는 nonisolated 함수 측면에서는 다시 actor에 의해 보호받는 상태에 접근하기까지 스레드 풀의 모든 스레드에서 자유롭게 실행될 수 있고, 대기 중인 다른 코드 측면에서는 actor에 접근하여 동시에 작업을 진행할 수 있습니다.

     

     nonisolated 함수 내부에서 공유 가변 상태에 대한 접근이 필요하다면, 다음과 같이 함수를 비동기 함수로 만든 다음, 호출(접근)에 대해서 await 키워드를 표시하여 일시 중단할 수 있음을 알리면 됩니다.

     

    actor CounterActor {
        var value = 0
        
        func increment() -> Int {
            value += 1
            let convetedValue = doSomething(value)
            return convertedValue
        }
        
        // Do something with value
        func doSometing() {
            ...
        }
        
        nonisolated func convert(_ value: Int) async -> String {
        	// 증가시킨 value를 가지고 가공을 하는 등 작업을 수행
            ...
            // 도중에 value(공유 가변 상태)에 접근해야 한다면(이 예는 조금 이상하지만)
            await doSomething()
            ...
        }
    }

     

     


     

    참고

    WWDC 2021 Protect mutable state with actors

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

     

    WWDC 2022 Visualize and optimize Swift concurrency

    (https://developer.apple.com/videos/play/wwdc2022/110350/)

    'iOS' 카테고리의 다른 글

    UIButton 커스텀(Image & Title)  (0) 2023.02.01
    UIKit에서 SwiftUI View 활용하기  (0) 2023.01.02
    Diffable DataSource  (2) 2022.12.29
    Localization in iOS  (0) 2022.12.14
    Operator, Combine Framework in Swift  (0) 2022.12.11

    댓글

Designed by Tistory.