Swift GCD에 대하여

황성진·2023년 12월 25일
0

Concurrency

목록 보기
1/2
post-thumbnail

이번 글은 스터디를 진행하면서 알게 된 내용입니다.
iOS 면접 질문에서 각자 원하는 주제를 설명하는 방식으로 진행되는데 제가 생각했을때 Concurrency에 대해서 이해하면 좋겠다고 생각해서 공부하게 되었습니다.
참고 강의 해당 유튜브 강의를 보면서 이해한 내용을 정리하였습니다.

동시성 프로그래밍 이란?

Concurrency 즉 동시성은 뭘까요?
해당 내용을 이해하기 위해서는 동시성에 대한 정의를 먼저 공부해야됐습니다.
동시성 프로그래밍 이란 간단하게 많은 수의 코어를 효율적으로 활용하는데 도움을 주는 것 입니다.
여러개의 코어를 사용해서 처리 속도를 높이거나, 중요하지 않은 작업을 중요도가 낮은 스레드에서 실행시키는 등 성능적인 이점을 갖고 있습니다.



쓰레드

컴퓨터를 구매하게 되면 4코어 8쓰레드, 8코어 16쓰레드 등의 성능을 많이 보게 됩니다.
쓰레드는 간단하게 말씀드리면 일을하는 녀석 이라고 정의 할 수 있습니다.

예를 들어 쓰레드가 하나라면 하나의 쓰레드에서 여러가지 일을 수행하게 됩니다.
반면 쓰레드가 8개 라면 Thread1에서 처리하는 동안 Thread2, Thread3 ... ,Thread8 이 일은 다른 쓰레드에서 분산처리 할 수 있습니다.



GCD(Grand Central Dispatch)에 대해서

Swift에서 Concurrency에 대해 검색하고 찾아보면 GCD와 비교를 많이하게 됩니다.
근데 초보자의 관점에서는 GCD가 뭔지 모르는 경우가 많아 GCD에 대해 먼저 정의하고 시작하고자 합니다.
GCD는 Swift Concurrency 가 등장하기 전 사용되는 동시성 프로그래밍 API 입니다.
GCD를 사용하면 async로 작업을 수행하고 나서 보통 탈출 클로저를 이용한 completion handler를 통해 해당 작업이 끝났을 때의 처리를 해주게 됩니다.

Dispatch Queue

  • Dispatch Queue는 FIFO Queue의 형태로 작업을 순서대로 전달 받습니다.


serialQueue vs. concurrentQueue

let serialQueue = DispatchQueue(label: "sungjin.serial")
let concurrentQueue = DispatchQueue(label: "sungjin.concurrent", attributes: .concurrent)

print(serialQueue) // Serial Dispatch Queue
print(concurrentQueue) // Concurrent Dispatch Queue

  • Dispatch Queue는 실행 방법에 따라 Serial QueueConcurrent Queue로 구분되어집니다.
    • Serial Queue는 추가된 작업을 하나씩 처리합니다.
      - 두개 이상의 작업이 동시에 처리되지 않기 때문에 Queue 기반의 동기롸 작업에서 많이 사용합니다.
      - DisPatch Queue를 생성할때 아무런 옵션을 부여하지 않으면 Se![]
      rial Queue로 생성 됩니다.
    • Concurrent Queue는 추가된 작업을 동시에 처리 합니다.
      • 동시에 실행되는 작업의 수는 시스템의 상태에 따라 자동적으로 처리 됩니다.


Sync vs Async

각 queue는 Sync와 Async 두 가지로 나눌 수 있습니다.

  • Sync는 말그대로 동기, Async는 말 그대로 비동기 입니다.
  • Sync는 큐에 추가된 작업이 종료 될 때 까지 기다리는 것
  • Async는 큐에 작업을 추가하지만 완려 여부를 보장하지 않는 것


main vs. global()

func forLoop(_ log: String) {
  var result: Int = 0
  for _ in 1...5 {
    result += 1
  }
  print("\(log) = \(String(result))")
}

// Main queue: global이 실행된 이후 순차적으로 실행된다.
DispatchQueue.main.async {
  forLoop("mainAsync")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  forLoop("mainAfterAsync")
}

순차적으로 실행 되는 것을 볼 수 있습니다.

  • mainQueue는 main thread에서 동작하는 특별한 Dispath Queue 입니다.
    • 따라서 UI update 관련 작업은 반드시 mainQueue에서 실행해야 합니다.
    • Serial Queue에 해당 됩니다.
    • 앱 생성 시점에서 자동으로 생성되기 때문에 언제든 DispatchQueue.main으로 접근할 수 있습니다.
// Global queue: 순서 상관없이 병렬 실행된다.
// 1과 2는 순서 상관없이 print 된다.
DispatchQueue.global().async {
  forLoop("globalAsync1")
}
DispatchQueue.global().async {
  forLoop("globalAsync2")
}

순서와 상관없이 실행되는 것을 볼 수 있다.

  • global() 메서드는 background thread에서 동작시킬 작업을 관리하는 Dispatch Queue를 리턴합니다.
  • global(qos:)값을 부여하여 그에 해당하는 Dispatch Queue를 받아올 수도 있습니다.
    • QOS 우선 순위

      1. userInteractive
      2. userInitiated
      3. default
      4. utility
      5. background
      6. unspecified



GCD 사용시 주의해야 할 사항

1. UI관련 작업은 반드시 메인큐에서 처리

DispatchQueue.global().async {
    // 다양한 작업 (UI 제외)
    DispatchQueue.main.async {
        // UI 작업
    }
}



2. Completion Handler의 사용

iOS에서 비동기(Async) 처리 방식은 해당 작업을 기다리지 않고 다음 작업을 진행하는 방식입니다.

이러한 방식은 작업을 분산 처리하여 성능을 높인다는 장점을 가지고 있지만,
특정 작업의 함수 결과물을 의존/사용하는 다른 작업이 존재할 경우 에러가 발생할 수 있습니다.

잘못된 함수 설계

var resultName: String?

func myName(name: String) -> String?{  // n번 쓰레드에서 작업
    DispatchQueue.global().async {
        sleep(2)
        resultName = name
    }
    return resultName
}

print(myName(name: "황성진"))

// 출력 결과 = nil  
// myName() 함수의 작업이 끝나기 전에 print() 함수의 작업이 진행하기 때문에 nil(에러) 출력



올바른 함수의 설계

var resultName: String?

func myName(name: String, completionHandler: @escaping (String?) -> Void){  // n번 쓰레드에서 작업
    DispatchQueue.global().async {
        sleep(2)
        resultName = name
        completionHandler(resultName)
    }
}

myName(name: "황성진") { item in
    print(item)
}

// 출력결과 : 황성진



3. Sync 함수를 Async로 동작하는 함수로 변형

작업 시간이 긴 함수들을 동기함수로 만들면 1번 쓰레드(Main Thread)에 과부하가 걸립니다.

이러한 이유로 작업 시간이 긴 함수를 내부에 비동기적 처리 하여 비동기로 동작하는 함수로 변형해야 합니다.

Sync 함수

func myTask(programmingLanguage: String) -> String{
    print("수학 숙제 시작")
    sleep(2)
    print("수학 숙제 종료")
   
    print("\(programmingLanguage) 코딩 공부 시작")
    sleep(2)
    print("코딩 공부 종료")
   
    print("영어 숙제 시작")
    sleep(2)
    print("영어 숙제 종료")
   
    return "공부 종료"
}

myTask(programmingLanguage: "Swift")

Async로의 변경

func myTask(programmingLanguage: String) -> String{
    print("수학 숙제 시작")
    sleep(2)
    print("수학 숙제 종료")
   
    print("\(programmingLanguage) 코딩 공부 시작")
    sleep(2)
    print("코딩 공부 종료")
   
    print("영어 숙제 시작")
    sleep(2)
    print("영어 숙제 종료")
   
    return "공부 종료"
}

func asyncMyTask(programmingLanguage: String, completionHandler: @escaping (String) -> Void){
    DispatchQueue.global().async {
        let function = myTask(programmingLanguage: programmingLanguage)
        completionHandler(function)
    }
}

asyncMyTask(programmingLanguage: "Swift") { item in
    print(item)
}

참고자료

GCD(Grand Central Dispatch) 정리
Dispatch Queue(GCD) 사용 시 주의사항

profile
직업군인에서 iOS 개발자로

0개의 댓글