본문 바로가기

iOS

[iOS] GCD 활용하기 1편 (DispatchQueue)

이전 포스팅에서 GCD의 기본 개념을 살펴보았다면 이제 예제 코드를 통해서 활용하는 법을 알아보도록 하겠습니다.

 

GCD는 iOS의 Concurrency 프로그래밍의 근간을 이루는 기술입니다. Dispatch Queue는 자동으로 스레드를 생성하고 효율적으로 관리합니다. Thread pool을 통해서 Thread를 재사용하기 때문에 시스템 리소스를 적게 사용하고 성능 또한 빨라집니다.

 

GCD는 직관적이고 단순한 API를 제공합니다. 모든 Apple 플랫폼(iOS, macOS, watchOS, tvOS)에서 동일한 API를 활용 가능하다는 것도 큰 장점입니다.

 

GCD의 핵심 객체는 Dispatch Queue입니다. Dispatch Queue에 Block 형태로 추가하거나 WorkItem으로 캡슐화해서 추가하여 작업을 실행하게 합니다. Concurrent Queue는 동시에 실행되는 작업을 다룹니다. 동시에 실행되는 작업 수는 시스템에 의해 자동으로 결정됩니다. Dispatch Queue는 직접 생성하거나 시스템이 제공하는 객체를 사용합니다.

 

먼저 Dispatch Queue를 직접 생성하는 법을 알아보겠습니다.

 

let serialQueue = DispatchQueue(label: "SerialQueue")
let concurrentQueue = DispatchQueue(label: "ConcurrentQueue", attributes: .concurrent)

 

생성자의 label은 해당 큐의 구분값으로 사용됩니다. label외에 다른 인자를 전달하지 않으면 기본적으로 Serial Queue가 생성됩니다. attributes에 .concurrent로 지정해주면 Concurrent Queue를 생성할 수 있습니다. Serial Queue는 추가된 순서대로 하나씩 작업을 실행합니다. 동시에 실행되지 않으므로 큐 기반 동기화에 자주 사용됩니다.

 

이번에는 시스템에서 기본적으로 제공하는 mainglobal Dispatch Queue를 알아보겠습니다.

 

@IBOutlet weak var textLabel: UILabel!

@IBAction func basicPattern(_ sender: Any) {
    // Action 메소드는 메인 큐에서 실행됨
    // 기본적으로 제공되는 Concurrent Queue
    DispatchQueue.global().async {
        var total = 0
        for num in 1...100 {
            total += num
        }

        // 기본적으로 제공되는 Serial Queue
        // UI Update는 항상 메인 큐에서 실행되어야함
        DispatchQueue.main.async {
            textLabel.text = "\(total)"
        }
    }
}

 

위의 형태가 가장 많이 쓰이고 기본적인 Dispatch Queue의 활용 형태입니다. Main Queue는 메인 스레드에서 동작하는 특별한 Serial Dispatch Queue 입니다. 앱 시작 시점에 자동으로 생성됩니다. 특히 화면에 보이는 UI의 업데이트는 메인 큐에서 실행되어야 합니다.

이번에는 syncasync의 차이점을 알아보도록 하겠습니다. 먼저 sync 메소드 입니다.

 

concurrentQueue.sync {
    for _ in 0..<3 {
        print("For-Loop")
    }
    print("Here 1")
}

print("Here 2")

// For-Loop
// For-Loop
// For-Loop
// Here 1
// Here 2

 

sync로 추가된 작업들이 모두 끝날 때까지 대기한 후 다음 코드가 실행되는 것을 알 수 있습니다. Lock과 유사한 기능을 구현할 때 사용되기도 합니다. sync 메소드를 메인 큐에서 사용할 경우 크래쉬가 날 수 있으므로 주의하시길 바랍니다.

 

주로 사용되는 async 메소드를 살펴보겠습니다.

 

concurrentQueue.async {
    for _ in 0..<3 {
        print("For-Loop")
    }
    print("Here 1")
}

print("Here 2")

// Here 2
// For-Loop
// For-Loop
// For-Loop
// Here 1

 

sync와는 다르게 async로 추가된 작업들을 기다리지 않고 바로 다음 코드가 실행되는 것을 알 수 있습니다. 여기서 정확하게 짚고 넘어가야할 부분이 있습니다. sync와 async 메소드는 DispatchQueue에 작업을 추가하는 메소드이지 작업을 실행하는 메소드가 아닙니다. 실제로 작업을 실행하는 것은 Dispatch Queue입니다.

 

sync와 async, 두 메소드가 Dispatch Queue의 동작 방식에는 영향을 주지 않습니다. Serial 큐에 작업을 추가할 때는 어떤 메소드를 사용하던지 항상 추가된 순서대로 실행됩니다. 반면에 Concurrent 큐에 추가할 때 sync 메소드를 사용하면 작업이 추가된 순서대로 하나씩 실행되는 것 같지만 Concurrent 큐작업을 동시에 실행할 수 있다는 점은 달라지지 않습니다. 때문에 다른 곳에서 Concurrent 큐에 작업을 추가하면 현재 실행중인 작업과 동시에 실행되게 됩니다.

 

Dispatch Queue가 작업을 실행할 때 Delay를 줄 수도 있습니다.

 

DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
    print("Here 1")
}

print("Here 2")

// Here 2
// Here 1 (3초 뒤에 출력됨)

 

이번에는 반복문을 Dispatch Queue의 concurrentPerform 메소드를 이용하여 빠르게 실행시키는 방법을 알아보도록 하겠습니다.

 

// 순서가 중요한 경우
var start = DispatchTime.now()

for index in 0..<20 {
    print(index, separator: " ", terminator: " ")
    Thread.sleep(forTimeInterval: 0.1) // 지연시간
}

var end = DispatchTime.now()

print("실행시간: ", Double(end.uptimeNanoseconds - start.uptimeNanoseconds) / 1000000000)
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// 실행시간:  2.019027588

// 순서가 중요하지 않은 경우
start = .now()

DispatchQueue.concurrentPerform(iterations: 20) { (index) in
    print(index, separator: " ", terminator: " ")
    Thread.sleep(forTimeInterval: 0.1) // 지연시간
}

end = .now()

print("실행시간: ", Double(end.uptimeNanoseconds - start.uptimeNanoseconds) / 1000000000)
// 1 3 2 4 0 7 6 8 9 10 5 11 12 13 15 14 16 19 17 18
// 실행시간:  0.202660094

 

실행시간을 비교하기 위하여 0.1초의 강제 지연시간을 주었습니다. 실행시간으로 알 수 있듯이 속도에는 차이가 분명한 것을 알 수 있습니다. 순서가 중요하지 않은 반복문이라면 concurrentPerform을 활용하면 더 빠른 성능을 낼 수 있습니다.

정리

  • 시스템에서 기본적으로 제공하는 Dispatch Queue는 main과 global이 있습니다.
  • main은 Serial Queue이고 global은 Concurrent Queue 입니다.
  • sync, async 메소드는 Dispatch Queue에 작업을 추가하는 메소드 입니다.
  • 실제로 작업은 Dispatch Queue가 실행합니다.
  • 순서가 중요하지 않은 반복문은 concurrentPerform을 활용하여 성능을 높일 수 있습니다.