iOS - Multi-Threading

728x90

Multi-Threading?

짧은 시간에 복잡하고 많은 작업을 실행하여 사용자에게 좋은 경험을 제공하기 위해,
작업을 여러 프로세스에서 동시에 실행시키는 병렬 처리 방법!

iOS에서도 역시 Multi-Threading을 지원한다.

 

iOS에서 사용하는 Multi-Threading

대표적인 3 가지!

  • GCD (Grand Central Dispatch)
  • Operation, OperationQueue (NSOperation)
  • Swift Concurrency

먼저 하나씩 알아보자!

 

GCD (Grand Central Dispatch)

https://developer.apple.com/documentation/dispatch
GCD는...

  • iOS 8.0+
  • 저수준 큐 기반
  • 멀티코어 환경과 멀티스레드 환경에서 최적화된 프로그래밍을 할 수 있도록 애플이 개발한 기술
  • 어떻게 사용할지 (일의 순서, 동기/비동기 처리) 정해주면 스레드를 관리해준다.
  • GCD 사용은 Dispatch라는 프레임 워크를 사용하고 DispatchQueue라는 클래스를 주로 사용한다.

사용법

// 동기, sync 
DispatchQueue.main.sync { ... } 
DispatchQueue.global().sync { ... } 
// 비동기, async 
DispatchQueue.main.async { ... } 
DispatchQueue.global().async { ... }

자세한 내용은 이전에 정리했던 노션 글 참조! (야곰 강의 정리본)
https://galvanized-jeep-eb7.notion.site/Concurrency-Programming-cc51f4320fd149acab2e44e20d3ba647

 

Operation, OperationQueue (NSOperation)

https://developer.apple.com/documentation/foundation/operation
Operation은...

  • iOS 2.0+
  • GCD를 객체지향적으로 재탄생 시킨것
  • 동시성 프로그래밍에 관련한 작업을 Operation이라는 객체로 만들어 놓고 사용할 수 있다.
  • Operation을 상속받아 커스텀 클래스로 직접 만들거나 BlockOperation이라는 하위 클래스를 사용해 사용가능
  • GCD에도 작업을 객체로 만드는 DispatchWorkItem이 있다... 다른점은?
    • 일단 Operation은 DispatchWorkItem 보다 먼저 나옴
    • Operation은 객체지향적으로 설계되어 더 세부적인 스케쥴링 가능
    • 세부적인것 중 대표적인 부분은 Operation 상태추적을 위한 다양한 프로퍼티가 존재함

사용법

let operation = BlockOperation { ... } // 생성
operation.addExecutionBlock { ... } // 첫번째로 초기화 한 위 작업이 끝나면 실행됨.
operation.completionBlock = { ... } // ExecutionBlock이 모두 실행된 뒤 호출되는 Property

operation.start() // 직접 실행하는 방식

let operationQueue = OperationQueue() //OperationQueue에 넣어 실행하는 방식
operationQueue.addOperation(operation)

자세한 내용은 이전에 정리했던 노션 글 참조! (야곰 강의 정리본)
https://galvanized-jeep-eb7.notion.site/Operation-3df4b450f95a4db985b5c193838c6b30

 

Swift Concurrency

https://developer.apple.com/documentation/swift/concurrency
Concurrency는...

  • iOS 13.0+
  • 구조적 동시성(Structured Concurrency)을 지향
  • GCD의 복잡함을(콜백) 줄이고 가독성, 안전성을 보장한 비동기 코드를 작성할 수 있도록 만듦.
  • 스레드, 큐를 직접 다루지 않아도 비동기 작업을 간단하고 명확하게 구현가능.
  • 직관적인 문법제공 - async, await, Task, actor 등의 키워드

이것도 이전 스터디 노션 정리내용으로!
강의영상 저작권 때문에 링크는 X


잘 몰랐지만 직접 쓰레드를 생성하는 방법도 있었다!

let current = Thread.current
print("current thread", current, current.stackSize)

let newTread1 = Thread()
newTread1.name = "secondThread"
print("secondary thread with default size", newThread1, newThread1.stackSize)

let newTread2 = Thread()
newThread2.name = "thirdThread"
newThread2.stackSize = 4096 * 512
print("secondary thread with default size", newThread2, newThread2.stackSize)


/* Thread class also has...*/
Thread.isMainThread //Bool

 

DispatchQueue, OperationQueue의 차이

  DispatchQueue OperationQueue
작업 간 의존성 설정 불가능 addDependency 메서드 사용해 가능
작업 취소 기능 수동구현 cancel로 명시적 취소 가능
우선순위 설정 qos로 제한적 가능 queuePriority qualityOfService 설정으로 상세하게 가능
재사용/재시작/중단 불가능 Operation으로 가능
결론 간단한 작업에 적합 복잡한 작업에 적합

 


Race Condition

Race Condition은 두 개이상 스레드가 하나의 자원에 동시에 접근 및 수정하려 할 때 발생하는 문제

그럼 각 Multi-Threading 방식에서는 어떻게 방지할 수 있는지 알아보자

 

GCD

Serial Queue 사용
Serial Queue를 사용하면 같은 큐에 실행되는 작업은 순차적으로 실행되기 때문에 동시접근이 발생 안함

let serialQueue = DispatchQueue(label: "com.example.myQueue")

serialQueue.async {
    // 공유 자원에 안전하게 접근
}

Dispatch Semaphore 사용

운영체제 과목을 공부하다보면 나오는 Semaphore와 같은 개념
특정 자원에 접근 가능한 스레드 수를 제한하는 방식

하나의 스레드가 접근하여 작업을 하는동안 다른 스레드는 대기하다가 들어갈 수 있게 되면 작업을 시작함.

let semaphore = DispatchSemaphore(value: 1) //접근 가능한 수 설정으로 이해하면 됌
func updateSharedResource() { 
    semaphore.wait() // 접근 시작 
    sharedResource += 1 
    semaphore.signal() // 접근 종료 
}

 

Operation

Operation 의존성 사용

그냥 동시에 실행되지 않게 실행순서를 보장하는 방법

let op1 = BlockOperation { /* 공유 자원 수정 */ }
let op2 = BlockOperation { /* 이후 작업 */ }

op2.addDependency(op1)
queue.addOperations([op1, op2], waitUntilFinished: false)

NSLock 사용

Lock을 사용해 명시적으로 접근을 제어가능

let lock = NSLock()
let operation = BlockOperation {
    lock.lock()
    // 공유 자원 접근
    lock.unlock()
}

 

Swift Concurrency

Actor 사용
actor는 내부 상태를 단일 태스크만 접근 가능하도록함.

actor Counter {
    private var count = 0

    func increment() {
        count += 1
    }

    func value() -> Int {
        return count
    }
}

// 사용
let counter = Counter()
await counter.increment()
let current = await counter.value()

메인 스레드에서 UI 업데이트를 해야 하는 이유는 무엇일까?

일단 iOS에서 메인 스레드는 UI, event 처리를 모두 담당하고 있다.

 

좋은 사용자 경험 및 일관성을 위해,
프레임워크 자체가 메인스레드에서 안전하게 작동되도록 설계되었기 때문이다.

 

UI업데이트를 병렬로 처리한다면 Race Condition이 발생 할 수 있다.
물론 방지하는 방법을 쓰면 괜찮겠지만, 어쩔 수 없이 지연이 생기게 될 것이다.
그렇게되면 일관성 및 사용자 경험이 저해되는 상황이 발생하게 된다.

 

번외로 메인스레드에서 오래걸리고 복잡한 로직을 (UI아님) 실행하게 된다면
메인 스레드 블락 현상이 발생한다. 그럼 화면 터치도 안되고 앱 자체가 작업이 끝날때 까지 멈추게 된다.
이를 방지하기 위해 메인스레드에서 UI 또는 이벤트 작업 외에는 동시성을 사용하는게 좋다.

 

 

728x90