Swift, Concurrency

Henry Lee·2022년 1월 12일
0

Concurrency(동시성)

동시성 프로그래밍이라고도 한다. Concurrency는 여러 작업을 나누어서 처리하는 것으로, 우리가 사용하는 아이폰이 노래도 재생하면서, 유저의 입력에 따라 이메일도 작성할 수 있고, 중간에 전화도 받을 수 있는 것이 이 동시성이다.
만약 노래를 재생하는 동안은 화면을 터치할 수 없고, 인터넷 검색도 불가능하다면, 굉장히 불편할 것이다.

코드로 실제로 어떻게 되는지 보자

//1
func calculatePrimes() {
    for number in 0...1_000_000 {
      let isPrimeNumber = isPrime(number: number)
      print("\(number) is prime: \(isPrimeNumber)")
    }
  }
  
//2
func isPrime(number: Int) -> Bool {
    if number <= 1 {
      return false
    }
    if number <= 3 {
      return true
    }
    
    var i = 2
    while i * i <= number {
      if number % i == 0 {
        return false
      }
      i = i + 2
    }
    return true
 }

calculatePrimes()는 1에서 1백만 까지의 자연수 중에 소수 를 구하는 메서드로, 해본 사람은 알겠지만 꽤 오랜 시간이 걸리는 작업이다.

var body: some View {
    VStack {
      Spacer()
      DatePicker(selection: .constant(Date())) {
        Text("Date")
      }
      .datePickerStyle(.wheel)
      .labelsHidden()
      Button {
        calculatePrimes()
      } label: {
        Text("Calculate Primes")
      }
      Spacer()
 }

화면에는 DatePicker UI가 있고, 버튼을 누르면 소수를 구하기 시작한다.

소수 구하기 버튼을 누르면 해당 계산을 하느라 DatePicker의 UI가 동작하지 않는다.
그 이유는 DatePicker의 UserInteraction과 calculatePrimes()를 둘다 메인 쓰레드에서 수행하고 있기 때문이다. calculatePrimes()가 쓰레드를 점유하고 있어서 다른 메인 쓰레드에서 작업해야 하는 User Interaction이 동작하지 않는 것이다.

Thread/Multithread

멀티쓰레드 라는 얘기 많이 들어보았는데, 그럼 쓰레드는 뭘까,

컴퓨팅에서 쓰레드는 OS 스케줄러에 의해 독립적으로 관리될 수 있는 프로그래밍된 명령의 가장 작은 시퀀스다.

요즘은 CPU는 다수의 코어와 그 이상의 쓰레드를 가지고 있어, 물리적으로 작업을 동시에 처리할 수 있다. 이게 멀티 쓰레딩이다.
이 부분이 Parallelism과 차이점이다. 병렬프로그래밍이라고 하는 Parallelism는 작업의 단위가 스레드가 아니라, CPU가 된다. 앞서 caculatePrimes()메서드를 하나의 스레드에서 점유해서 계산하지 않고, 분할해서 계산해서 작업의 속도를 높이는 것이 Parallelism이고
Concurrency의 핵심은 스레드이다.

Context Swiching

Concurrency의 또 하나의 핵심은 Context Switching으로, 하나의 코어는 Time Slicing이라는 방법을 통해 Concurrency하게 움직인다. 쉽게 이해해보면, 내가(아이폰) 커피를 만드는데 이 커피를 만드는 동작안에는 원두를 분쇄하고, 물을 끓이고, 컵을 준비하는 일련의 동작이고, 이를 아주 빠르게 한다, 다른 사람(사용자) 입장에서 보면 거의 동시에 일어나는 것 처럼 보인다.

Use Concurrency

그래서 다시 맨 처음 어플리케이션으로 돌아와서, iOS는 기본적으로 멀티쓰레드이다. 소수를 구하는 것과 동시에 UI작업을 처리할 수 있는 것이 당연한 것이다. 그런데 실제로는 계산을 하느라 UI작업을 하지 못했다. 우리는 소수를 구하는 작업을 다른 쓰레드에서 하도록 명시할 필요가 있다.

애플은 고맙게도? 쓰레드를 편리하게 사용할 수 있는 프레임워크를 제공한다. 굉장히 Low하게 보면 NSThread와 더 내려가서 Unix POSIX 쓰레드를 통해서 이 쓰레드를 사용할 수 있지만 우리에게는 Grand Central Dispatch(GCD)가 있다. GCD 또한 Concurrency 작업을 위한 프레임워크로 꽤 LowLevel 측면으로 디자인 되어있다.

또다른 옵션은 Operation Queue로 GCD위에 만들어져 있고, 더 쉽고 간결한 코드를 제공한다.

그 다음은 2019년에 나온 Combine으로 이는 백그라운드에서 작업을 선언형으로 관리한다. 오퍼레이터를 통해 스레드 간의 쉬운 전환이 가능하다.

또! 그다음은 Swift Concurrency가 있다.

GCD

GCD는 스레드 위에 구축되며, 공유 스레드 풀을 관리한다. 이를 사용해서 DispatchQueue에 코드 블록을 처리하고 GCD는 이를 실행할 쓰레드를 결정하게 된다.

Queue

Queue는 FIFO구조로 먼저 들어온 작업을 먼저 내보낸다는 특징이 있다. GCD는 같은 방식으로 작업 순서를 보장한다. 이때 Queue는 Seiral이거나 Concurrent일 수 있다.

이처럼 Serial은 선형 시간동안 하나의 작업만 실행된다. Task1이 끝나면 Task2가 실행되는 식이다. 쓰레드를 사용해서 이를 Concurrent하게 할 수 있다.

Concurrent는 Task순서대로 일단 작업을 할당하긴 했지만 Serial과는 다르게 Task2보다 Task3과 Task4가 훨씬 더 빨리 끝나서 작업물을 반환한다. 그래서 작업 순서를 보장할 수 없다.
커피를 다 볶은 다음에 컵에 넣고 물을 부어야 하는데, 물이 먼저 끓었다고 물부터 냅다 부어버린 것

Operation Queue

GCD를 사용하기 위해서는 Operation을 상속해서 만들 수 있다.

class CaculatePrimeOperation: Operation {
  
  
  override func main() {
    
    for number in 0...1_000_000 {
      let isPrimeNumber = isPrime(number: number)
      print("\(number) is prime: \(isPrimeNumber)")
    }
  }
  
  func isPrime(number: Int) -> Bool {
    if number <= 1 {
      return false
    }
    if number <= 3 {
      return true
    }
    
    var i = 2
    while i * i <= number {
      if number % i == 0 {
        return false
      }
      i = i + 2
    }
    return true
  }
  
}

main()에서 앞에서 작성한 소수 구하기 메서드를 이식한다.

let operation = CaculatePrimeOperation()
func calculatePrimes() {
  
  let queue = OperationQueue()
  queue.addOperation(operation)

}

그런 다음 해당 클래스의 인스턴스를 만든 뒤, OperationQueue의 인스턴스에addOperation으로 넘겨준다.

  func calculatePrimes() {
    let operation = CalculatePrimeOperation()
    let queue = OperationQueue()
    queue.addOperation(operation)
  }

사실 꼭 Operation을 상속하는 클래스를 만들 필요는 없고 addOperation이 클로져를 제공하기 때문에 메서드를 해당 클로져 안에 작성해주면 된다.

OperationQueue는 인스턴스를 만들 때 자동으로 남는 쓰레드를 할당해 준다. 만약 메인 쓰레드를 사용하라고 명시적으로 알리고 싶다면

let mainQueue = OpearationQueue.main

으로 쓸 수 있다.

OperationQueue말고도 더 쉽고 간편한 방법은 DispatchQueue가 있다

DispatchQueue

DispatchQueue.global(qos: .userInitiated).async {
  for number in 0...1_000_000 {
    let isPrimeNumber = number.isPrime
    print("\(number) is prime: \(isPrimeNumber)")
  }
}

쓰레드를 global로 보낼 수 있다. qos설정은 애플 문서에서 자세히 확인할 수 있다.
https://developer.apple.com/documentation/dispatch/dispatchqos/qosclass

그래서 이제 앞에서 소수를 구하느라 반응하지 않던 UI를 구조할 수 있게 되었다.

Swift Concurrency

이 작업을 Swift Concurrency를 이용하면 코드 한줄? 추가하는 것으로 가능하다.

  func calculatePrimes() {
    doneLabel = "Calculating!"
    Task {
      for number in 0...1_000_000 {
        let isPrimeNumber = number.isPrime
        print("\(number) is prime: \(isPrimeNumber)")
      }
    }
    doneLabel = "Done!"
  }

Refereces

profile
iOS Dev, Coffee in my bloodstream

0개의 댓글