본문 바로가기

iOS/Concurrency

[Swift] GCD 개념 정리 | No7. GCD

 

안녕하세요.

이번 포스트는 GCD를 공부하면서 배운 개념들을 전부 정리하려고 합니다.

 

1. Concurrency and Parallelism

  • Concurrency and Parallelism 개념 정리 포스트 1 편, 2 편

 

Parallelism은 하드웨어, 기계적 성질입니다. Concurrency와 마찬가지로 multi task를 할 수 있습니다. 다만 multi core일 때 가능합니다. 반면 concurrency는 single core, multi core 둘 다 concurrency한 동작을 할 수 있습니다. time-slicing과 context switch를 통해 task 수행 전환이 가능합니다. 

 

개발을 하다보면 concurrent task 실행이 반드시 필요합니다. Concurrency task를 할 수 있는 방법은 한 개 또는 여러 개 이상의 thread를 구현 하는 것입니다. Thread를 관리할 수 있는 thread pool도 커스텀으로 구현해야 합니다. 특정 task가 많은 thread를 필요로 할 때 thread pool에서는 thread의 수를 할당, 해제해야 하는데 이런 solution에 대한 어려움이 있습니다. Cpu가 발전함에 따라 Apple은 multi core를 효율적으로 활용하고 싶어했습니다. 그래서 concurrency를 수행할 수 있는 API를 개발했고 그 중 하나가 GCD입니다.

2. GCD(Grand Central Dispatch)

 

GCD라 불리는 Dispatch는 apple의 framework입니다. Multi core인 cpu 하드웨어를 잘 활용해 concurrent한 code 수행이 가능합니다. 앱에서 동작되는 모든 task는 기본적으로 main thread에서 실행됩니다. Main thread와 연관 있는 main queue는 UIApplication에서 발생되는 모든 UIResponder에 대해서 감지하고 연관된 delegate, event handler를 호출합니다. 계산작업, 비교적 수행속도가 오래 걸리는 task가 main queue에 추가되어 main thread에서 실행됬을 때 UIApplication에서 발생되는 사용자의 이벤트 터치, UI업데이트 등이 먹히지 않아 앱은 freezing 상태가 됩니다.

 

Multi cure를 제대로 활용하기 위해 여러 thread를 사용하고자 GCD는 shared thread pool을 관리합니다. 또한 concurrent한 task를 수행할 수 있는 threads 위에 추상적인 개념을 도입했습니다. queue와 task, QoS와 같은 model을 제공합니다. 그중 DispatchQueues, RunLoops가 기본베이스입니다. Dispatch(GCD)는 dispatch queues를 관리합니다. shared thread pool에서 Queue 안에 있는 tasks를 어느 thread에 사용할 것인지 결정합니다. system level에서 수행합니다!! 그렇기에 system resources, 어떤 therad를 사용할지 등등 결정합니다.  

 

Shared thread pool을 관리함으로 동시간대에 여러 task를 main thread가 아닌 other thread에서 수행함으로 concurrent한 실행을 할 수 있고 앱의 반응성은 좋아집니다.

 

task..?(==DispatchWorkItem) queue..?(==DispatchQueue)

 

3. DispatchQueue

 

DispatchQueue는 queue의 구조를 갖습니다. Task를 enqueue합니다. Task는 코드의 블럭 형식이라 할 수 있습니다. 클로저, 함수나 DispatchWorkItem으로 감싸서 DispatchQueue에 dispatch 보냅니다. FIFO 구조를 갖고 queue의 type이 존재합니다.

 

Serial vs concurrency 두 가지 type을 지원하고 있습니다.

Queue는 main, global, custom 세가지 종류가 있습니다.

 

3-1. DispatchQueue.main

DispatchQueue.main 의 경우 serial입니다. main thread와 같이 단 한 개. UI draw, update는 main queue에서 진행해야 합니다. Serial인 특징 답게 특정 시간대에 1개의 task만 실행할 수 있습니다. task를 효율적으로 다른 queue에 배분하지 않으면 GCD에서의 설명 때 처럼 앱은 freezing(애니메이션은 느려지고, 사용자의 터치는 늦게 반응하는 등..) 합니다. Main queue는 UI draw or 사용자의 input등에 관련된 task만 처리하도록 해야합니다. Concurrent queue에서 수행된 task의 결과 중 UI update를 할 경우에도 main queue를 사용합니다.

 

async()를 통해 task를 추가하는데 async의 매개변수(레이블)은 @escaping 클로저입니다. 곧바로 실행될 수도 있지만 async()함수가 반환된 후에 나중에 실행될 수 있습니다(async).

 

3-2. DispatchQueue.Global(), custom queue(private dispatch queue)

그럼 networking, computation, data fetch 등의 task는 어디서 수행되야 할까요? Global queue or custom queue(DispatchQueue type)에서 수행하면 됩니다. 물론 queue의 특징은 FIFO입니다. GCD는 main thread와 같이 background thread도 단 한 개의 thread로 하진 않습니다. Global, custom queue에서 할당되는 thread는 한 개 또는 여러 개의 thread에 할당됩니다. 물론 queue를 매번 만들 때마다 thread가 할당되진 않고 때론 재사용 되기도 합니다. DispatchQueue.global() or custom queue를 통해 main thread와 concurrent 한 작업을 할 수 있습니다. 

 

3-2-1. DispatchQueue.Global()

DispatchQueue.Gloabl()의 경우 concurrency 특징을 갖고 있습니다. 왜냐하면 main thread와 같이 background에서 실행될 수 있기 때문입니다. background thread라고 불립니다. 하지만 한 가지 특징은 main thread를 제외한 thread의 경우 task가 실행된다는 보장이 없습니다. 

 

그 예로 GCD No3. 포스트에서도 알 수 있었는데, global queue에서 sync 타입인 task를 실행했을 경우 main thread는 아무것도 할 수 없게 됩니다. 그렇기에 main thread가 gloabl queue의 sync task를 실행하게 됩니다. ( 정확하진 않습니다. )

 

3-2 초반에 언급한 여러 task(Network,fetch...)가 있습니다. 각 task마다 수행시간이 다릅니다. 어떤 task는 길고 다른 task는 짧습니다. Queue의 특성은 FIFO입니다. 물론 concurrent하게 실행을 하긴 합니다. 그럼에도 concurrent 작업을 수행할 때는 동시성 문제가 있습니다. 이를 해결하기 위한 방법은 task에 우선순위를 지정하는 것입니다. 물론 이마저도 우선순위가 낮은 task의 자원이 우선순위가 높은 task가 자원을 필요로 하면 가져갈 수 있습니다. 

 

애플은 QoS(Quality-of-Service)를 통해 queue의 task에 우선순위를 부여했습니다.

  • User-Interactive
  • User-Initiated
  • Utility
  • background

 

적절한 우선순위를 부여해 main thread와 같이 concurrent task를 수행하는 코드를 짰을 때 multi core는 뿌듯해 할 것입니다.

 

3-2-2. DispatchQueue's custom queue(private dispatch queue)

custom queue의 경우 concurrent, serial 두 attributes의 작업 환경을 queue에 세팅할 수 있습니다. label을 통해 어느 queue인지 식별 가능합니다. 

 

커스텀 큐 attributes가 serial이라면 해당 queue의 thread는 특정 시간대에 1개의 task만 실행합니다. (prev task가 끝나야 next task 실행 가능) 물론 다른 queue와 concurrent task를 실행할 수 있습니다(다른 thread일 경우). Dispatch는 queue에 있는 task를 실행할 때 각기 다른 thread를 사용하기 때문입니다. (때론 재사용하기도 합니다.)

 

커스텀 큐 attributes가 concurrent라면 연관 thread는 concurrent tasks 수행이 가능합니다.

 

4. DispatchWorkItem

 

DispatchQueue에 추가되는 task는 closure, func 타입을 사용하는 async(), sync()함수 이외에도 DispatchWorkItem으로 감싸서 task를 queue에 보낼 수 있습니다. wrap되지 않은 task보다 DispatchWorkItem은 많은 기능을 사용할 수 있습니다.

 

5. DispatchGroup

 

DispatchQueue는 여러 task를 enqueue합니다. 그리고 수행합니다. 여기서 아쉬운 점은 queue에 있는 모든 task가 끝났을 때를 알 수 있는 점이 없다는 것입니다. 이는 GCD(Dispatch)에서 지원되는 DispatchGroup를 사용하면 됩니다. DispatchGroup은 task를 queue에 추가할 때 같이 group에 추가합니다. Group에 추가된 task들은 추적됩니다. 그리고 group에 있는 모든 task가 끝났을 경우 completion handler로 notify()함수를 호출함으로 notification을 합니다.

 

중요한 것은 task를 queue에 추가될 때 group도 추가해야 하는데 task마다 queue 타입이 달라도 상관 없습니다. 이게 장점인 것 같습니다. notify(queue:work:) or wait()를 통해 특정 group 내 모든 task가 끝남을 알 수 있습니다. 각각 async, sync 특징을 갖고 있습니다.

 

6. Concurrency programming시 발생되는 문제

  • Concurrency problem 개념 정리 포스트

공유자원은 존재하고 concurrency를 통한 readers-writers가 있는 한 concurrency problem이 발생합니다. 이에 주의해야 합니다.

data race? -> DispatchWorkItem의 flags type을 barrier와 sync 한 write, read 사용으로 해결할 수 있습니다. 공유자원을 사용할 때도 마찬가지로 barrier를 활용해 해결할 수 있습니다. 아니면 DispatchSemaphore를 통해서 해결할 수 있습니다.

 

 

 

결론:

GCD를 통해 main thread의 부담을 덜어줄 수 있습니다. concurrency programming을 할 수 있습니다.

 

GCD를 공부하며 느낀점: 

통신에서의 asynchronous란 정보를 한번에 다량으로 보내지 않고 데이터를 여러 블록으로 쪼갠 후 상황에 따라 블록을 비동기적으로 보냅니다.

 

그러나 DispatchQueue에서 사용되는 asynchronous는 약간 다른 개념인 것 같습니다. queue의 async()함수에 task를 클로저로 추가합니다. 그리고 queue에 전달합니다. 중요한 것은 queue의 async()는 @escaping 키워드(개념 정리 포스트)를 사용한 클로저를 정의합니다.

 

Thread는 Top - down 방식의 코드 흐름에 맞게 DispatchQueue.큐타입(main or global or cusotm queue).async() { ... } 함수를 실행합니다. 그렇게 async()함수가 실행됩니다. 물론 함수의 클로저 타입 매개변수 "{ ... } 클로저" 또한 실행되야 하지만 실행을 하지 않고 바로 return. 종료합니다. 클로저 안에 있는 task를 바로 실행하지 않습니다.

 

"그럼 클로저를 작성한 이유가 없지 않나요...?"

 

클로저는 실행 안한 채로 queue.async()함수가 반환되어 메모리에서 할당해제 됩니다. 하지만 @escaping 키워드를 붙인 덕에 클로저는 이 생명주기를 움켜잡고 있는 async()함수가 메모리에서 해제되어도 클로저의 내용은 저장(capture) 됩니다. 그리고 언젠간 실행됩니다. escaping closure는 자기자신 또는 밖에 있는 property를 참조할 때 string reference cycle의 형태를 띕니다.

 

그래서 async()가 종료 되어도 언젠간 해당 클로저가 실행합니다. 근데 실행될 때 해당 내용이 전부 실행됩니다. 이 점 덕분에 좋은 점은 async()함수의 매개변수에 넣은 task가 main thread에서 곧바로 실행되지 않고 다른 thread에서 실행되기 때문에 main thread와 같이 concurrent한 작업을 할 수 있다는 점입니다.

 

하지만 @escaping을 사용한 async()함수는 통신에서 표현하는 asynchronously communication과는 사뭇 다른것 같습니다. task를 여러번 쪼개서 실행하는게  아니라 나중에 background thread에서 언젠가 실행될 때 한방에 실행하는 느낌입니다. 그럼에도 동시성인 점은 background thread가 여러 개로 할당될 수 있기 때문이고 각각의 async, sync task는 main thread와 같이 동시에 실행될 수 있다는 점! 

 

이 포스트 작성 이후에 스윞트 포럼에 글을 올렸습니다. 그리고 답변을 받았습니다. ( 바로 비하인드 scene보러 달려가야겠습니다 :)

 

만약 여러 thread에서 각각의 tasks가 실행되는데 빨리 작업되야 하는, 많은 system resources가 필요한 작업이 들어왔을 때, 각각의 thread에서 실행중인 tasks는 양보하지 않고 자원을 점유한 채로 계속해서 실행할 것입니다...

 

 

Swift의 async/await은 concurrenct한 프로그램을 만들 수 있는데 네트워크 통신에서 말하는 asynchronously적인 느낌의 실행을 합니다. GCD의 async()함수처럼 언젠가 다시 쭉 실행하는게 아니라 일단 async task를 마주하면 await! 정지합니다. 언제 실행되는지는 system의 상태에 따라 결정됩니다.

 

System은 await된 task가 실행되도 좋은 상황이라고 판단하면 실행합니다. 그리러다 다른 task를 해야하면 suspend, -> resume을 반복하면서 실행을 합니다. 그리고 실행이 완료되면  Top-down 방식으로 해당 async task 다음 line코드가 실행됩니다.

 

WWDC에서 소개한 async, await는 정말 asynchronous한 실행을 하는 것 같습니다. 정확하게는 자세하게 공부를 한 후 정리를 해보려고 합니다!!

 

 

 

이 포스트에서 틀린 내용 발견시 댓글로 알려주신다면 정말 감사합니다.