GCD의 활용 1 (DispatchQueue) - ehrldyd15/Swift_Skills GitHub Wiki

DispatchQueue

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는 직접 생성하거나 시스템이 제공하는 객체를 사용한다.

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

생성자의 label은 해당 큐의 구분값으로 사용된다.

label외에 다른 인자를 전달하지 않으면 기본적으로 Serial Queue가 생성된다.

attributes에 .concurrent로 지정해주면 Concurrent Queue를 생성할 수 있다.

Serial Queue는 추가된 순서대로 하나씩 작업을 실행한다. 동시에 실행되지 않으므로 큐 기반 동기화에 자주 사용된다.

이번에는 시스템에서 기본적으로 제공하는 main과 global 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의 업데이트는 메인 큐에서 실행되어야 한다.

이번에는 sync와 async의 차이점을 알아보도록 하자

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을 활용하면 더 빠른 성능을 낼 수 있다.

정리

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

자료출처

https://onelife2live.tistory.com/4?category=741789