[iOS] Concurrency Programming (동시성 프로그래밍) 2 : GCD, DispatchGroup, DispatchWorkItem

2023. 3. 15. 04:25iOS/iOS

728x90
반응형

👉 이전 글

 

[iOS] Concurrency Programming (동시성 프로그래밍) 1 : Sync (동기), Async (비동기)

👉 시작 프로그래밍을 하면서 가장 어려웠던 부분이 비동기 처리였던 거 같다. 예전에 비동기라는 개념을 몰랐던 학생시절에 프로젝트를 만들면서 엄청난 고생을 했던 적이 떠오른다 ,, ㅎㅎ

nlestory.tistory.com

 

 

👉 시작

 

위의 글에서는 동시성 프로그래밍과 비슷한 개념에 대해서 공부했다.

동시성 프로그래밍을 하기 위해서 iOS에서 제공하는 기술인 GCD와 Operation가 있다

이번에는 GCD에 대해서 공부해보았다.

 

 


👉 GCD (Grand Central Dispatch) = Dispatch

  • 시스템에서 관리하는 디스패치 큐에 작업을 제출하여 멀티코어 하드웨어에서 동시에 코드를 실행한다.
  • Grand Central Dispatch(GCD)라고도 하는 Dispatch에는
    macOS, iOS, watchOS 및 tvOS의 멀티코어 하드웨어에서 동시 코드 실행 지원에 대한 체계적이고 포괄적인 개선 사항을 제공하는 언어 기능, 런타임 라이브러리 및 시스템 향상 기능이 포함되어 있다.
  • Dispatch Queue를 사용하여 Multi Thread를 지원

 

 

👉 Dispatch Queue

  • 앱의 기본 쓰레드 또는 백그라운드 쓰레드에서 순차적으로 또는 동시에 작업 실행을 관리하는 개체이다.
  • task를 비동기적으로 동시에 수행할 수 있는 쉬운 방법 중 하나이다.
  • 큐는 FIFO 구조로 디스패치큐 또한 FIFO 구조를 가지고 있다.

 

 

👉 Dispatch Queue 종류

✔️  Main Queue

  • 현재 프로세스의 메인 쓰레드와 관련된 디스패치큐
  • Serial Queue의 특성을 가지고 있다.
  • Task 들을 순차적으로 처리하며 1번에 1개의 Task만 처리한다.
  • mainQueue 가 메인 쓰레드로 작동하고 있어 앱에는 하나만 존재한다.
  • UI관련 작업은 무조건 mainQueue 에서 작업해줘야 한다.
DispatchQueue.main.async {
}

 

UI 관련 작업은 main.async 에서 호출하는 이유는?
UIKit 클래스는 메인쓰레드에서 사용해야하기 때문에 main큐에 넣어줘야한다.

DispatchQueue.main.sync 를 호출하면 ?
mainQueue 는 Serial Queue로
sync를 호출하게 되면 앱의 이벤트를 처리하고 있던 메인쓰레드가 sync 호출로 멈추게 되고 deadlock(교착상태)가 발생한다.

 

 

✔️  Global Queue

  • QoS 클래스가 있는 글로벌 시스템 큐를 반환한다.
  • Concurrent Queue의 특성을 가지고 있다.
  • 동시에 여러 개의 Task를 처리한다.
  • 데이터 통신 등 끝나는 시간을 알 수 없는 작업, main Queue에서 동작하는 작업에 영향을 끼치지 않아야 할 때 등 비동기 처리를 하기 위해서 사용된다.
DispatchQueue.global(qos: .userInteractive).sync {
}
DispatchQueue.global().async {
}
  • Qos를 지정하지 않으면 (qos: .Default) 와 같다.

 

 

✔️  Custom Queue

  • 기존에 만들어져있는 큐말고 우리가 원하는 대로 생성하여 만드는 큐이다.
let queue = DispatchQueue(label: "lablename", qos: .userInitiated, attributes: .concurrent)
  • label : 큐의 이름을 설정해준다. 디폴트 값이 없기 떄문에 반드시 작성해야 한다.
  • qos : QoS를 설정한다. 디폴트 값은 default 이다.
  • attributes : 디폴트 값은 Serial 이다.

 

✔️  사용 예제

DispatchQueue.global().async {
    for i in 1...10 {
        sleep(1)
        print("\(i)")
    }
}

비동기 방식으로 1초에 하나씩 출력한다.

 

DispatchQueue.global().async {
    for i in 1...10 {
        sleep(1)
        print("\(i) 🧡 ")
    }
}

DispatchQueue.global().async {
    for i in 1...10 {
        sleep(1)
        print("\(i) 💛 ")
    }
}

두 가지의 같이 비동기로 진행한 것이다.

이떄 주황색을 비동기, 노란색을 동기로 바꿔주게 되면

DispatchQueue.global().sync {
    for i in 1...10 {
        sleep(1)
        print("\(i) 💛 ")
    }
}

출력결과는 같지만 동기방식으로 10초동안 화면이 멈추게 된다.

화면이 나오지 않고 버튼이나 UI를 만질 수 없게 된다.

 

화면의 UI를 변경하거나 할 때는 Main에서 변경해야한다.

DispatchQueue.global().async {
    for i in 1...10 {
        sleep(1)
        print("\(i) 💛 ")
        DispatchQueue.main.async {
            self.label.text = "\(i)"
        }
    }
}

중간에 버튼을 누르면 버튼이 작동되는 것을 볼 수 있고 UILabel의 텍스트도 변경된다.

 

 

 

👉 Quality of Service (QoS)

  • 시스템이 실행을 위해 작업을 예약하는 우선순위를 결정하는 값이다.
  • 아래로 갈수록 우선순위가 낮아진다.
userInteractive 중요도가 높고 즉각적인 반응이 요구되는 작업 (UI업데이트, 이벤트핸들링) 일 때 사용
userInitiated userInteractive보다 덜 중요하지만 사용자가 빠른 결과를 기다릴 때 사용
default 기본 값 (우선 순위를 신경쓰지 않는 중요한 정도가 상관 없을 때)
utility 시간이 걸리는 작업, 즉각적인 결과가 필요하지 않을 때 사용
background 백업같은 사용자에게 당장 보이지 않아도 될 일을 처리할 때 사용
unspecified Qos 정보가 없음을 나타냄

 

 

✔️  사용 예제

DispatchQueue.global(qos: .userInteractive).async {
    for i in 1...10 {
        print("\(i) 💚 ")
    }
}

DispatchQueue.global(qos: .userInteractive).async {
    for i in 1...10 {
        print("\(i) 💙 ")
    }
}

같은 우선순위를 가질 때에는 먼저 실행되는 것과 먼저 끝나는 것을 알 수 없다.

 

하지만 우선순위를 다르게 줄 경우에는 작업의 속도가 달라진다.

초록색하트를 가장 낮은 우선순위로, 파란색하트를 가장 높은 우선순위를 주었다.

그리고 먼저 실행한 것이 먼저 끝날 수도 있다는 생각에 낮은 우선순위를 먼저 실행하였다.

DispatchQueue.global(qos: .background).async {
    for i in 1...10 {
        print("\(i) 💚 ")
    }
}

DispatchQueue.global(qos: .userInteractive).async {
    for i in 1...10 {
        print("\(i) 💙 ")
    }
}

보면 파란색이 먼저 실행되고 먼저 끝나는 것을 알 수 있다.

초록색은 언제 실행되는 지는 모르지만 끝나는 게 파란색보다는 늦게 끝나게 된다.

먼저 바뀌어야 할 경우에는 우선순위를 높게 주어서 먼저 끝날 수 있게 구현해야 한다.

 

 

✔️  Completion 사용

func function1(completion: @escaping(String) -> ()) {
    DispatchQueue.main.async {
        var result = 0
        for i in 1...10 {
//                sleep(1)
            print("\(i) ❤️ ")
            result = i
        }
        completion("결과 \(result)")
    }
}

func function2() {
    DispatchQueue.main.async {
        for i in 1...10 {
//                sleep(1)
            print("\(i) 🧡 ")
        }
    }
}

메소드로 사용할 경우 해당 메소드가 끝나는 시점을 알고 싶을 경우 completion을 사용한다.

String 타입을 받아서 completion()에 넣어주면 해당 메소드가 끝난 후 파라미터를 넘겨준다.

function1() { result in
    print(result)
}
function2()

비동기처리가 끝났을 때 가져올 결과를 입력하면 된다.

 

 

 


👉 Dispatch Group

  • 단일 단위로 모니터하는 작업의 그룹이다.
  • 여러 작업 항목을 그룹에 연결하고 동일한 큐 또른 다른 큐에서 비동기식으로 실행하도록 예약한다.
  • 모든 작업 항목이 완료되면 그룹은 completion handler를 실행한다.
  • 그룹의 모든 작업이 실행을 완료할 때까지 동기적으로 기다릴 수 있다.
  • 디스패치 그룹은 여러 개의 작업이 모두 끝난 하나의 시점을 알 수 있다.

 

✔️  사용 방법

1. DispatchGroup을 생성한다.

let dispatchGroup = DispatchGroup()

 

2-1. async로 group 파라미터에 그룹을 추가한다.

DispatchQueue.global().async(group: dispatchGroup) {
    //task
}
DispatchQueue.main.async(group: dispatchGroup) {
    //task
}

다른 쓰레드로 보내는 경우에도 그룹으로 묶을 수 있다.

2-2. enter/leave 를 이용하여 그룹에 추가한다.

dispatchGroup.enter()
DispatchQueue.global().async() {
    //task
}
DispatchQueue.main.async() {
    //task
}
dispatchGroup.leave()

 

3-1. notify 를 이용하여 끝나는 시점을 알 수 있다. (비동기 방식)

dispatchGroup.notify(queue: .global()) {
    print("완료")
}
dispatchGroup.notify(queue: .main) {
    print("완료")
}

main과 global 큐가 아닌 커스텀큐를 만들어서 추가하였다면 queue 파라미터에 커스텀큐의 이름을 작성하면 커스텀큐의 완료시점만을 알 수 있다.

notify는 비동기 방식으로 완료를 처리하지만 동기방식으로 완료를 처리할 수도 있다.

wait을 사용하여 해당 그룹의 작업이 완료할 때까지 기다리게 한다. 

그룹 작업이 다 끝나야지만 다음 작업을 할 수 있는 상황에서 완료를 비동기로 응답할 수 없는 경우에 사용된다.

wait() 만 사용할 경우 무한정 기다리게 되기 때문에 timeout 파라미터를 이용한다.

timeout은 얼마나 기다릴 지를 정하는 파라미터이다. 일정 시간 후에 다음 작업이 진행된다.

3-2. wait 을 이용하여 기다린 후 끝나는 시점을 알 수 있다. (동기방식)

let timeOutResult = dispatchGroup.wait(timeout: .now() + 60)
if timeOutResult == .success {
    print("60초 안에 디스패치 그룹 완료")
}
if timeOutResult == .timedOut {
    print("60초 안에 디스패치 그룹 실패")
}

 

주의할 점은 동기방식(sync)은 메인 쓰레드에서 수행하면 안된다.
wait은 함수 호출한 쓰레드를 멈추기 때문에 다른 쓰레드에서 작업이 끝날 때까지 메인쓰레드가 멈추게 된다.

 

 

✔️  사용 예제

let dispatchGroup = DispatchGroup()
        
DispatchQueue.global(qos: .background).async(group: dispatchGroup) {
    for i in 1...10 {
        print("\(i) 💜 ")
    }
}

DispatchQueue.global(qos: .userInteractive).async(group: dispatchGroup) {
    for i in 1...10 {
        print("\(i) 🖤 ")
    }
}

DispatchQueue.global().async {
    let timeOutResult = self.dispatchGroup.wait(timeout: .now() + 2)
    if timeOutResult == .success {
        print("2초 안에 디스패치 그룹 완료")
    }
    if timeOutResult == .timedOut {
        print("2초 안에 디스패치 그룹 실패")
    }
}

글로벌큐에 보라색하트와 검정색하트와 그룹을 기다리는 작업을 넣고 비동기 방식으로 진행한다.

그러면 보라색하트와 검정색하트는 출력하는 기능을 구현하고 그룹을 기다리는 작업은 2초동안 기다리게 된다.

이떄 2초동안 기다리는 작업이 끝난 경우에 그룹의 작업이 끝났을 경우 .success의 결과를 출력하게 된다.

만약 2초동안 기다린 시간동안 그룹의 작업이 끝나지 않았을 경우에는 .timeOut의 결과를 출력하게 된다.

지정한 시간 후에 작업이 완료되지 않았어도 출력은 timeOut이 나오지만 시간이 흐르면서 작업이 멈추지 않고 계속 진행하게 된다.

 

검정색하트 출력하는 부분에서 sleep(1)을 추가하여 2초가 지나도 작업이 완료되지 않게 한다면 timeOut의 결과를 출력한다.

DispatchQueue.global(qos: .userInteractive).async(group: dispatchGroup) {
    for i in 1...10 {
        sleep(1)
        print("\(i) 🖤 ")
    }
}

1초에 한번씩 출력하게 되면 전체 작업이 걸리는 시간은 10초가 된다. 이때 2초뒤에는 작업이 완료되지 않는다.

그룹에 있는 작업이 모두 성공하지 못했기 때문에 .timeOut 이 출력되고 그 뒤에도 작업은 계속 진행하게 된다.

 

✔️  주의 사항

▶️ 그룹 지정을 정확하게 해줘야 한다.

그룹을 지정하거나 enter/leave를 사용하지 않았을 경우에는 그 해당 작업은 그룹에 속하지 않는다. 전체 작업이 끝나는 것을 의도하였다면 모든 작업에 그룹을 지정해줘야한다.

DispatchQueue.global(qos: .userInteractive).async(group: dispatchGroup) {
    for i in 1...10 {
        print("\(i) 🖤 ")
    }
    DispatchQueue.global().async {
        for i in 1...10 {
            print("\(i) ❤️ ")
        }
    }
}

빨강색 하트가 검정색하트의 하위 작업일지라도 그룹명을 지정해주지 않았을 경우 빨강색하트는 그룹에 속하지 않는다.

같은 그룹에 넣어주기 위해서는 그룹명을 입력하는 것을 잊지 않아야 한다.

 

▶️ wait 호출을 메인 쓰레드에서 하면 안된다.

wait 이라는 메소드는 작업을 멈추고 기다리게 하기 때문에 메인 쓰레드에서 사용하게 되면 UI도 멈추게 된다.

그룹 내의 작업이 wait이 실행되는 쓰레드로 할당되어서도 안된다. 이 상황도 마찬가지고 wait이 실행되는 쓰레드는 멈춰있는 상태이고 그룹내의 작업이 여기에 할당된다면 같이 멈춰서 데드락이 발생하기 때문이다.

검정색하트가 1초에 하나씩 출력하게 진행하였고 wait을 이용하여 10초동안 기다리게 하였다.

DispatchQueue.global(qos: .userInteractive).async(group: dispatchGroup) {
    for i in 1...10 {
        sleep(1)
        print("\(i) 🖤 ")
    }
}

let timeOutResult = self.dispatchGroup.wait(timeout: .now() + 10)
if timeOutResult == .success {
    print("10초 안에 디스패치 그룹 완료")
}
if timeOutResult == .timedOut {
    print("10초 안에 디스패치 그룹 실패")
}

검정색하트를 출력하는 작업은 글로벌큐에 넣어서 작동하고

디스패치그룹이 기다리는 작업은 메인쓰레드에서 작업을 하게되기 때문에 지정한 시간인 10초동안 UI가 멈추게 된다.

검정색하트가 출력하는 동안 10초가 지났을 때 그룹의 작업이 완료되지 않았기에 timeOut의 결과를 출력하게 되고 이때 10초동안 기다린 후이기 때문에 여기서부터 화면이 움직인다.

커스텀큐를 만들어서 작업을 커스텀큐에서 실행하고 wait은 글로벌큐에 진행해도 결과는 정상적으로 작동된다.

let customQueue = DispatchQueue(label: "customQueue", qos: .default, attributes: .concurrent)
customQueue.async(group: dispatchGroup) {
    for i in 1...10 {
        sleep(1)
        print("\(i) 🤍 ")
    }
}

DispatchQueue.global().async {
    let timeOutResult = self.dispatchGroup.wait(timeout: .now() + 10)
    if timeOutResult == .success {
        print("10초 안에 디스패치 그룹 완료")
    }
    if timeOutResult == .timedOut {
        print("10초 안에 디스패치 그룹 실패")
    }
}

위의 검정색하트의 결과는 화면이 10초동안 기다리게 되서 버튼이 작동하지 않지만 

아래 흰색하트의 경우는 wait의 기능도 비동기 방식으로 진행하고 메인쓰레드에 영향을 주지 않기 때문에 작업이 진행되는 동안에도 버튼 클릭이벤트가 정상적으로 잘 작동하게 된다.

 

 

 


👉 Dispatch WorkItem

  • 수행하려는 작업은 completion handle 또는 실행 종속성을 연결할 수 있는 방식으로 캡슐화된다.
  • Dispatch WorkItem은 디스패치큐 또는 디스패치그룹 내에서 수행할 작업을 캡슐화한다.
  • 작업항목을 DispatchSource 이벤트, 등록 또는 취소 핸들러로 사용할 수 있다.
클로저 클래스

왼쪽은 클로저로 묶어 사용하던 방법이고 오른쪽은 workItem을 이용하여 클래스로 묶어사용하는 방법이다.

 

✔️  사용 방법

1. workItem을 생성한다.

let dispatchWorkItem = DispatchWorkItem(qos: .userInitiated) {
    //Task 1
    print("작업")
}

기존의 방식과 마찬가지로 우선순위 설정도 할 수 있다.

 

2-1.  동기 방식으로 실행한다.

dispatchWorkItem.perform()

perform 은 workitem의 인스턴스 메소드로 현재 쓰레드에서 해당 workItem을 동기적으로 실행한다.

2-2.  비동기 방식, 디스패치큐에 추가한다. 

DispatchQueue.global().async(group: dispatchGroup, execute: dispatchWorkItem)

기존의 방식과 마찬가지로 group 을 설정할 수 있다.

workItem은 excute 를 사용하여 큐에 추가해준다.

 

 

✔️  사용 예제

1. 동기 방식

let redWorkItem = DispatchWorkItem(qos: .userInitiated) {
    for i in 1...1000 {
        print("\(i) ❤️ ")
    }
}
let orangeWorkItem = DispatchWorkItem(qos: .userInitiated) {
    for i in 1...1000 {
        print("\(i) 🧡 ")
    }
}
redWorkItem.perform()
orangeWorkItem.perform()

극적인 효과를 보기위해 1000까지의 수로 진행하였다. 빨강색하트를 먼저 실행하고 그다음 주황색하트를 실행하였다.

수동으로 perform을 이용하여 동기적으로 실행한 결과이다.

동기적이기에 빨강색하트의 작업이 끝날 때까지 기다렸다가 다음 주황색하트의 작업을 시작한다.

해당 작업은 메인쓰레드에서 진행하기 때문에 작업이 끝날 때까지 화면이 작동하지 않는다. (1000까지 진행하는 것도 순식간에 끝나기 때문에 출력하는 중간에 sleep(1)을 작성하여 구현하면 화면이 멈추는 지 알 수 있다.)

 

2. 비동기 방식, 디스패치큐에 추가

DispatchQueue.global().async(execute: redWorkItem)
DispatchQueue.global().async(execute: orangeWorkItem)

글로벌큐에 추가하여 실행하면 비동기적으로 실행되기 때문에 빨강색하트와 주황색하트가 출력된다.

결과 사진은 출력 중간을 캡쳐한 사진이다. 우선순위가 같기 때문에 어느 것이 먼저 끝나는 지는 알 수 없다.

글로벌큐에 추가하였기 때문에 화면도 잘 돌아가기때문에 버튼이벤트도 작동이 된다.

 

 

👉 Dispatch WorkItem 기능

✔️  작업을 취소하는 기능

dispatchWorkItem.cancel()

workItem에는 cancle 이라는 메소드가 존재한다. 말 그대로 작업을 취소하는 메소드이다.

하지만 작업 중에는 cancle 이 호출되어도 작업이 없어지는 것이 아니라 isCancelled 의 속성이 바뀌는 것이다.

guard dispatchWorkItem.isCancelled else {
    return
}

isCancelled 의 속성을 이용하여 작업을 더 이상 진행하지 않게 하거나 다른 처리를 하면 된다.

 

 

✔️  순서를 정하는 기능

DispatchGroup 에서 종료시점을 알려주는 notify 메소드를 통해서 순서 기능을 구현할 수 있다.

notify로 작업1이 끝났을 경우 작업2를 시작하도록 지정할 수 있다.

let dispatchWorkItem1 = DispatchWorkItem(qos: .userInitiated) {
    //Task 1
    print("작업1")
}
let dispatchWorkItem2 = DispatchWorkItem {
    //Task 2
    print("작업2")
}
DispatchQueue.global().async(execute: dispatchWorkItem1)

dispatchWorkItem1.notify(queue: DispatchQueue.global(), execute: dispatchWorkItem2)

dispatchWorkItem1 의 작업이 끝나면 알려주고 dispatchWorkItem2 를 실행하는 것이다.

이렇게 순서를 지정하여 작업을 처리할 수 있다.

let redWorkItem = DispatchWorkItem(qos: .userInteractive) {
    for i in 1...10 {
        print("\(i) ❤️ ")
    }
}
let orangeWorkItem = DispatchWorkItem(qos: .userInteractive) {
    for i in 1...10 {
        print("\(i) 🧡 ")
    }
}

DispatchQueue.global().async(execute: redWorkItem)
redWorkItem.notify(queue: DispatchQueue.global(), execute: orangeWorkItem)

왼쪽은 notify를 작성하지 않았을 때 오른쪽은 notify를 작성했을 경우의 결과이다.

레드가 끝나는 것을 알려주면 그 다음에 실행시킬 작업을 입력하면 그 작업을 실행하게 된다.

 

 


📂 정리

Main Queue

  • Serial(씨리얼) 큐 -순서대로 하나씩 처리
  • UI와 관련된 작업은 여기서 구현해야 한다.

 

Global Queue

  • Concurrent(컨커런트) 큐 -여러가지 작업을 동시에 처리
  • QoS로 우선순위를 결정한다.

 

Dispatch Group

  • 그룹에 묶인 작업들의 끝나는 시점을 한 시점으로 묶고 싶을 때 사용한다.
  • 그룹을 사용할 때의 주의할 점
    • 그룹 이름 지정을 잘 해준다.
    • 메인 쓰레드에서 사용하지 않는다.

 

Dispatch WorkItem

  • 작업을 취소시킬수 있다.
  • 작업의 순서를 지정할 수 있다.

 

 

 

👉 다음 글

 

[iOS] Concurrency Programming (동시성 프로그래밍) 3 : Operation

👉 시작 2023.03.15 - [iOS/iOS] - [iOS] Concurrency Programming (동시성 프로그래밍) 2 : GCD, DispatchGroup, DispatchWorkItem [iOS] Concurrency Programming (동시성 프로그래밍) 2 : GCD, DispatchGroup, DispatchWorkItem 👉 시작 2023.03.1

nlestory.tistory.com

 

 

 

 

 

 

 

 

 

 

[예제 소스코드 깃허브 링크]

https://github.com/HANLeeeee/PracticeTest/tree/main/DispatchTest

 

GitHub - HANLeeeee/PracticeTest

Contribute to HANLeeeee/PracticeTest development by creating an account on GitHub.

github.com

[참고자료]

Apple_Developer_Documentation_Dispatch

Apple_Developer_Documentation_DispatchQueue

Apple_Developer_Documentation_main

Apple_Developer_Documentation_global

Apple_Developer_Documentation_QoS

Apple_Developer_Documentation_DispatchGroup

Apple_Developer_Documentation_DispatchWorkItem

 

 

 

 

 

728x90
반응형