동시성

concurrency의 사전적 정의는 ‘동시성'입니다. 한번에 여러 개의 작업을 동시에 수행하는 것을 의미합니다.

쓰레드

thread는 concurrency를 직접적으로 구현할 수 있게 해주는 개념입니다. 각각의 쓰레드는 한번에 하나의 작업을 담당해서 수행합니다.

만약 하나의 쓰레드만 존재한다면 아래 그림에 있는 작업을 하나의 쓰레드에서만 수행하게 되면 네트워크나 이미지 수정, 파일 저장을 하는 동안 사용자 interaction을 처리할 수 없을 것입니다. 사용성이 떨어지는 앱이 될 수 밖에 없습니다.

각 쓰레드는 실제도 동시에 작업을 수행하지는 않지만 빠른 속도로 번갈아 가면서 수행합니다. (아래 형광펜으로 표시된 쓰레드가 작업 중인 쓰레드를 표시한 것입니다^^) 사용자에게는 동시에 실행된다고 느껴질 정도입니다.

메인 쓰레드

여러 개의 쓰레드 중에 가장 중요한 일을 담당하는 쓰레드를 메인 쓰레드라고 부릅니다. 앱에서 가장 중요한 것은 역시 사용자와의 interaction이겠죠? 메인 쓰레드는 사용자의 input을 받고 output을 UI에 표시하는 역할을 담당합니다.

메인 쓰레드는 가장 높은 우선순위를 가지기 때문에 사용자가 터치를 하면 바로 앱이 반응할 수 있습니다. 또한 코3드를 짤 때 UI 업데이트를 메인쓰레드가 아닌 다른 쓰레드에 넣게 되면 아래 그림처럼 에러가 발생합니다.

GCD (Grand Central Patch)

GCD는 Concurrency, 멀티쓰레딩을 iOS에서 쉽게 사용하기 위해서 한 단계 추상화한 개념입니다.

실행해야할 작업을 코드 블럭에 담아서 GCD에 전달하면 시스템이 알아서 쓰레드에 할당하고 실행해줍니다. 따라서 개발자가 직접 쓰레드에 접근할 필요가 없고 GCD를 잘 활용하면 됩니다.

Dispatch Queue

GCD를 사용하기 위해서는 Dispatch Queue에 작업을 보내면 됩니다.

Main Queue

메인 쓰레드에서 작동하는 큐입니다. 주로 UI 업데이트와 사용자 input을 처리합니다. 단지 메인 쓰레드를 명시적으로 사용하지 않더라도 다른 큐에 보내지 않는 작업은 모두 메인 큐에 보내지게 됩니다.

DispatchQueue.main.async {
	// 메인 쓰레드에서 실행할 작업
}

Global Queue

시스템에 의해서 관리되는 큐입니다. 작업의 중요도를 qos class를 통해서 정의할 수 있습니다. qos는 Quality Of Service를 의미합니다. 중요도는 아래 표를 참고해주세요.

DispatchQueue.global(qos: .userInteractive).async {
}

DispatchQueue.global(qos: .userInitiated).async {
}

DispatchQueue.global(qos: .default).async {
}

DispatchQueue.global().async {
    //✅ qos를 명시 안하면 default
}

DispatchQueue.global(qos: .utility).async {
}

DispatchQueue.global(qos: .background).async {
}

Custom Queue

직접 중요도와 이름을 지정해서 사용할 수 있는 큐입니다. 기본적으로 Serial Queue이지만 Concurrent Queue로 지정할 수도 있습니다.

let customConcurrentQueue = DispatchQueue(label: "customConcurrentQueue", qos: .background, attributes: .concurrent)
let customSerialQueue = DispatchQueue(label: "customSerialQueue", qos: .background)

Sync & Async

해당 큐에 보내는 작업을 동기적으로 실행할 것인지 비동기적으로 실행할 것인지 결정합니다. 동기적으로 실행하는 경우 해당 작업을 모두 실행하고 다음 줄의 작업을 실행합니다. 반대로 비동기적으로 실행하는 경우 해당 작업을 실행하면서 다음 줄의 코드도 동시에 실행합니다. playground에서 아래 코드를 실행해보면서 예시를 들어보겠습니다.

//비동기적 실행
DispatchQueue.global(qos: .background).async {
    for i in 0...5 {
        print("먼저 실행 \(i)")
    }
}

DispatchQueue.global(qos: .userInteractive).async {
    for i in 0...5 {
        print("나중에 실행 \(i)")
    }
}
// 실행 결과
먼저 실행 0
나중에 실행 0
나중에 실행 1
나중에 실행 2
나중에 실행 3
나중에 실행 4
나중에 실행 5
먼저 실행 1
먼저 실행 2
먼저 실행 3
먼저 실행 4
먼저 실행 5

비동기적으로 실행을 하면 먼저 실행한 코드의 print 문이 다 나오기 전에 다음 코드의 print문이 실행 되는 것을 볼 수 있습니다. 그리고 두번째 코드의 DispatchQueue 우선 순위가 더 높으므로 나중에 실행한 print문이 먼저 출력됩니다.

// 동기적 실행
DispatchQueue.global(qos: .background).sync {
    for i in 0...5 {
        print("먼저 실행 \(i)")
    }
}

DispatchQueue.global(qos: .userInteractive).async {
    for i in 0...5 {
        print("나중에 실행 \(i)")
    }
}
// 실행 결과
먼저 실행 0
먼저 실행 1
먼저 실행 2
먼저 실행 3
먼저 실행 4
먼저 실행 5
나중에 실행 0
나중에 실행 1
나중에 실행 2
나중에 실행 3
나중에 실행 4
나중에 실행 5

반면에 동기적으로 실행하면 먼저 실행한 print문이 다 출력되기 전에는 다음 줄의 코드가 실행되지 않습니다. 이 때는 우선 순위에 관계 없이 무조건 먼저 실행한 작업이 끝나고 나서 다음 코드를 실행합니다.

마치며...

  1. 개발을 하면서 동시성을 직접 적용해서 사용하는 경우는 드물었던 것 같습니다.
  2. 하지만 동시성은 iOS 앱의 많은 부분 특히 네트워크 쪽에서 기초가 되는 개념입니다.
  3. 콜백 함수 (completionHandler)를 인자로 받는 API들이 대부분 비동기적으로 구현된 것입니다.
  4. 다음 시간에는 ARC에 대해서 다뤄 보고자 합니다^^
  5. 너무 완벽하게 정리하기 보다는 꾸준하게 하는 것을 목표로 하겠습니다!
profile
교사 출신 iOS 개발자입니다. iOS를 사랑하고 교육 관련 비지니스, 코딩 교육에 관심이 많습니다. https://github.com/SteadySlower

0개의 댓글