티스토리 뷰

반응형

안녕하세요 :) Zedd입니다.

오늘은 Concurrency Programming Guide의 Dispatch Queues부분을 해볼게요!! 드디어 익숙한게 나왔........거의 모두들 GCD를 쓰실텐데.....


Dispatch Queues


Grand Central Dispatch (GCD) dispatch queues는 task 수행을 위한 강력한 도구입니다. dispatch queues를 사용하면 호출자(caller)와 관련하여 비동기/동기식으로 임의의 코드 블록을 수행 할 수 있습니다. dispatch queues를 사용하여 별도의 쓰레드에서 수행하는 거의 모든 task를 수행 할 수 있습니다. dispatch queues의 장점은 사용하기 쉽고, 해당 쓰레드 코드보다 해당 task를 실행 할 때 훨씬 효율적이라는 것입니다.

이 챕터에서는 dispatch queues를 사용하여 앱에서 일반 task를 실행하는 방법에 대한 정보를 제공합니다. 


About Dispatch Queues

dispatch queues은 앱에서 task를 비동기적으로 동시에 수행 할 수 있는 손쉬운 방법입니다. task는 앱이 수행해야하는 작업입니다. 예를들어, 일부 계산을 수행하거나 데이터 구조를 작성 또는 수정하고 파일에서 읽은 일부 데이터를 처리하거나 여러가지 task를 정의 할 수 있습니다. 함수 또는 블록 객체 내에 해당 코드를 배치ㅏ고 dispatch queues에 추가하여 task를 정의합니다. 

dispatch queues는 제출한 모든 task를 관리하는 객체와 유사한 구조입니다. 모든 dispatch queues은 FIFO데이터 구조입니다. 따라서 queues에 추가하는 task는 항상 추가된 순서대로 시작됩ㄴ디ㅏ. GCD는 자동으로 일부 dispatch queues를 제공하지만, 특정 용도로 작성 할 수 있는 다른 큐를 제공합니다. 다음은 앱에서 사용 할 수 있는 dispatch queues타입과 사용방법입니다. 


● SerialSerial queues(private dispatch queues라고 알려진)은 큐에 추가된 순서대로 한번에 하나의 task를 실행합니다. 현재 실행중인 task는 dispatch queues에서 관리하는 고유한 쓰레드(task마다 다를 수 있음)에서 실행됩니다. Serial queues는 종종 특정 자원에 대한 액세스를 동기화 하는데 사용됩니다. 필요한 만큼  Serial queues를 작성 할 수 있으며, 각 큐는 다른 모든 큐와 관련하여 동시에 작동합니다. 다시말해,  Serial queues을 4개 작성하면 각 큐는 한번에 하나의 task만 실행하지만, 최대 4개의 task가 각 큐에서 동시에 실행될 수 있습니다.  


● ConcurrentConcurrent queues(일종의 global dispatch queue라고도 알려진)은 동시에 하나 이상의 task를 실행하지만 task는 큐에 추가된 순서대로 게속 시작됩니다. 현재 실행중인 task는 dispatch queue에서 관리하는 고유한 쓰레드에서 실행됩니다. 특정 시점에서 실행되는 정확한 task의 수는 가변적이며 시스템 조건에 따라 다릅니다. iOS 5이상에서는 큐의 타입으로 DISPATCH_QUEUE_CONCURRENT를 지정하여 사용자가 동시에 dispatch queue을 생성 할 수 있습니다. 또한 앱에 사용할 사전에 정의된 global concurrent queues가 4개 있습니다.


● Main dispatch queue : main dispatch queue는 앱의 main 쓰레드에서 task를 실행하는, 전역적으로 사용 가능한 serial queue입니다. 이 큐는 앱의 실행루프와 함께 작동하여 큐에 있는 task의 실행을 실행루프에 연결된 다른 이벤트 소스의 실행과 얽힙니다. 앱의 main쓰레드에서 실행되므로, main queue는 종종 앱의 주요 동기화 지점으로 사용됩니다. main dispatch queue를 만들 필요는 없지만, 앱이 적절하게 배수(drains)되도록 해야합니다. 




앱에 동시성을 추가 할 때, dispatch queues는 쓰레드에 비해 몇가지 이점을 제공합니다. 가장 직접적인 이점은 work-queue(작업 큐) 프로그래밍 모델의 단순성입니다. 쓰레드를 사용하면 수행 할 작업과 쓰레드 자체의 생성 및 관리를 위한 코드를 모두 작성해야합니다. 

dispatch queues를 사용하면, 쓰레드 생성 및 관리에 대해 걱정 할 필요 없이 실제로 수행하려는 작업에 집중 할 수 있습니다. 대신 시스템이 모든 쓰레드 생성 및 관리를 처리합ㄴ디ㅏ. 이점은 시스템이 단일 앱보다 훨씬 효율적으로 쓰레드를 관리 할 수 있다는 것입니다. 시스템은 사용가능한 자원 및 현재 시스템 조건에 따라 동적으로 쓰레드 수를 조절 할 수 있습니다. 또한 시스템은 일반적으로 쓰레드를 직접 작성 한 경우보다 빨리 task를 실행 할 수 있습니다.


dispatch queues에 대한 코드를 작성하는 것이 어렵다고 생각 할 수도 있지만, 쓰레드에 대한 코드를 작성하는것보다 dispatch queues에 대한 코드를 작성하는 것이 더 쉽습니다. 코드 작성의 핵심은, 독립적이고, 비동기적으로 실행되는 task를 설계하는 것입니다.(이는 실제로 쓰레드 및 dispatch queues에 모두에 대해 해당됩니다.) 하지만 dispatch queues의 장점은 예측가능성(predictability)입니다. 동일한 공유 리소스에 액세스 하지만, 다른 쓰레드에서 실행되는 두가지 task가 있는 경우, 쓰레드가 먼저 리소스를 수정 할 수 있으므로, 두 task가 동시에 해당 리소스를 수정하지 못하도록 lock을 사용해야합니다. dispatch queues을 사용하면 한번에 하나의 task만 자원을 수정하도록 할 수 있습니다. 이 queue-based synchronization타입은 lock보다 효율적입니다. loack은 경쟁/비경쟁의 경우, 항상 고가의(expensive) 커널 트랩을 필요로 하는 반면, dispatch queues은 주로 앱의 프로세스 공간에서 작동하고, 절대적으로 필요한 경우(absolutely necessary)에만 커널을 호출하기 때문입니다.


serial queue에서 실행중인 두개의 task가 동시에 실행되지 않는다는 점을 지적하는 것은 옳지만,(Serial queue니까 하나씩 실행할거 아녀 ㅡㅡ 동시에 실행 안되니까 리소스를 동시에 수정할 일 없자너 ㅡㅡ 이렇게 지적한다는 말) 두개의 쓰레드가 동시에 잠금을 사용하면, 쓰레드가 제공하는 동시성이 손실되거나 크게 감소한다는 것을 기억해야합니다. 

더 중요한 것은 쓰레드 모델은 커널과 사용자 공간 메모리를모두 차지하는 두개의 쓰레드를 생성해야한다는 것입니다. dispatch queues은 쓰레드에 대해 동일한 메모리 페널티를 지불하지 않으며, 사용하는 쓰레드는 사용중이기 때문에 차단(block)되지 않습니다.

dispatch queues에 대해 기억해야 할 다른 주요 사항은 다음과 같습니다.

  • Dispatch queues는 다른 

    Dispatch queues와 관련하여 동시에 task를 실행합니다. task의 직렬화는 single dispatch queue의 task로 제한됩니다. 
  • 시스템은 한번에 실행되는 총 task수를 결정합니다. 따라서, 100개의 다른 큐에서 100개의 task를 가진 앱은(100개 이상의 유효코어가 없는 한) 모든 task를 동시에 실행 할 수 없습니다. 
  • 시작될 새 task를 선택 할 때, 큐 priority level이 고려됩니다. Serial queue priority를 결정하는 방법은 Providing a Clean Up Function For a Queue를 참고하세요.
  • 큐의 task는 큐에 추가 될 때, 실행할 준비가 되어있어야 합니다. 이전에 Cocoa Operation queue를 사용했다면, 이것이 model operation에 사용되는 것과 다르다는 점에 유의하세요
  • Private dispatch queues는 reference-counted객체입니다. 자신의 코드에 큐를 유지하는 것 외에도,  dispatch sources를 큐에 첨부될 수 있으며 retain count를 증가시킬 수 있다는 점에 유의해야합니다. 따라서 모든 dispatch sources가 취소(canceled)되었는지 확인하고, 모든 retain call이 적절한 release call과 균형을 이루도록 해야합니다. 

Queue-Related Technologies

dispatch queues외에도 Grand Central Dispatch를 사용하여 코드를 관리하는 데 도움이 되는 몇가지 기술을 제공합니다. 


Dispatch groupsDispatch groups은 완료(completion)을 위해 블록 객체 집합을 모니터링 하는 방법입니다. 그룹은 필요에 따라 동기적/비동기적으로 블록을 모니터링 할 수 있습니다. 그룹은 다른 task의 완료 여부에 따라 코드에 유용한 동기화 메커니즘을 제공합니다.


Dispatch semaphoresDispatch semaphores는 전통적인 세마포어와 유사하지만, 일반적으로 더 효율적입니다. 세마포어를 사용 할 수 없기 때문에 호출 쓰레드(calling thread)를 차단해야하는 경우에만 세마포어가 커널로 호출됩니다. 세마포어를 사용 할 수 있으면, 커널 호출이 수행되지 않습니다. 


Dispatch sources : dispatch source는 특정 타입 시스템 이벤트에 대한 응답(response)으로 notifications을 생성합니다. Dispatch sources를 사용하여 프로세스 notifications, 신호(signal) 및 descriptor events와 같은 이벤트를 모니터링 할 수 있습니다. 이벤트가 발생하면 Dispatch sources는 처리를 위해 지정된 dispatch queue에 비동기적으로 task코드를 제출(submit)합니다. 


Implementing Tasks Using Blocks

블록 객체는 C, Objective-C, 그리고 C++ 코드에서 사용 할 수 있는 C기반 언어기능입니다. 블록을 사용하면, 자체적인 작업단위(unit of work)를 쉽게 정의 할 수 있스비낟. 블록이 function pointers와 유사한 것 처럼 보일 수 있겠지만, (function pointers가 뭐지...?) 


(데이터 값을 가리키는 대신, 함수 포인터는 메모리 내에서 실행 가능한 코드를 가리킨다. 역참조될 때, 함수 포인터는 가리키는 함수를 보통의 함수 호출처럼 작동시키고 인자를 보내는데 사용될 수 있다. 이러한 작동은 함수가 고정된 이름이나 주소를 사용해서 직접적으로가 아니라 변수를 통해서 간접적으로 호출되기 때문에 또한 "간접" 호출로도 알려져 있다. 출처 : 위키백과

학교다닐 때 C++을 했었는데..정말 한번도 안쓴 기능이네요ㅎ)


아무튼 계속 고


블록이 function pointers와 유사한 것 처럼 보일 수 있겠지만, 실제로 블록은 객체와 유사한 기본 자료 구조로 나타내지며, 컴파일러에서 생성 및 관리합니다. (a block is actually represented by an underlying data structure that resembles an object and is created and managed for you by the compiler.)

컴파일러는 사용자가 제공한 코드를 (관련 데이터와 함께) 패키지화하고, heap에 상주하며 앱 주위로 전달 할 수 있는 형태로 캡슐화 합니다.


블록의 주요 장점 중 하나는 자신의 lexical scope외부에서 변수를 사용할 수 있다는 것입니다. 함수나 메소드 내부에 블록을 정의하면 블록은 어떤면에서 전통적인 코드 블록처럼 작동합니다. 예를들어, 블록은 상위범위에 정의된 변수값을 읽을 수 있습니다. 블록이 액세스 한 변수는 블록이 나중에 액세스 할 수 있도록 heap의 블록 데이터 구조에 복사됩니다. 블록이 dispatch queue에 추가되면, 값은 일반적으로 읽기전용(read-only)타입으로 유지되어야 합니다. 그러나 동기적으로 실행되는 블록은 __block키워드가 미리 지정된 변수를 사용하여 상위 호출 영역으로 데이터를 반환할 수 있습니다.


(참..........ㅎ 문서가 objc기반이다 보니까 objc키워드들이 나오네요ㅎ_ㅎ..아 지금생각난건데,,지금까지 이 문서 번역하면서 블록이라는말을 굉장히 많이 썼는데.. 블록은 swift의 closure라고 생각하시면 됩니다. 하지만 아주 중요한 다른점이 있으니 <Swift Closure vs. Objective-C Block 차이점 비교 분석> 참고해주세요! 아주 잘 정리하신듯..)

function pointers에 사용되는 구문과 유사한 구문을 사용하여 코드를 inline으로 선언합니다. 블록과 function pointers사이에 주요 차이점은 블록이름앞에 *대신 ^이 옵니다. function pointers와 마찬가지로, 인수를 블록에 전달하고, 그로부터 리턴값을 받을 수 있습니다. 다음 코드는 코드에서 동기적으로 블록을 선언하고, 실행하는 방법을 보여줍니다. 변수 aBlock은 단일 Int타입 매개변수를 취해 값을 반환하지 않는 블록으로 선언됩니다. 이 프로토타입과 일치하는 실체블록이 aBlock에 할당되고, inline으로 선언됩니다. 마지막 줄은 블록을 즉시 실행하여 지정된 Int를 표준 출력으로 print합니다. 


let x = 123

let y = 456


let aBlock: (Int)->() = { (z) in

    print(x,y,z)

}


aBlock(789)

//123 456 789


제가 원래 이런 문서 공부하면서 예제코드가 거의 진심 거의 대부분 Objc였는데, 그때마다 코드 쓸 일이생기면 Swift로 바꿔서 넣고는 해요! 물론 Concurrency Programming Guide앞부분들도 다 Objc였지만 Swift로 바꿔서 올렸는데..이건 순간 

아니 앞에 블록이니..__block이니...*대신 ^가 온댔는데...그니까 앞에 설명은 다 Objc로 해놓고 이렇게 Swift코드를 올리는 것이 괜찮은 것인가?라는생각이 드네요...............

혹시모르니 Objc코드도 첨부합니다. 


앞에는 그냥 블록을 어떻게 만드는지(이게 왜 Concurrency Programming Guide에 있는지는 모르겠지만) 였고 중요한 부분은 지금부터입니다.

다음을 블록을 사용할 때 고려해야 할 몇가지 핵심 지침을 요약한 것입니다. (이게 Objc에서만 이런건지..Swift에서도 이런건지 그거는 잘 모르겠네요.)


● dispatch queue를 사용하여 비동기적으로 수행하려는 블록의 경우, 상위 함수 또는 메소드에서 스칼라 변수를 캡쳐하여 블록에서 사용하는것이 안전합니다. 그러나 큰 구조체나 호출 컨텍스트에 의해 할당되고 삭제된 포인터 기반 변수를 캡쳐해서는 안됩니다. 블록이 실행될 때까지 해당 포인터가 참조하는 메모리가 사라질 수 있습ㄴ디ㅏ. 물론 메모리(또는 객체)를 직접 할당하고 해당 메모리의 소유권을 블록에 명시적으로 블록에 넘겨주는 것이 안전합니다. 


● dispatch queue는 추가된 블록을 복사하고, 실행이 끝나면 블록을 해제(release)합니다. 즉 블록을 큐에 추가하기 전에 블록을 명시적으로 복사 할 필요가 없습니다.


● 작은 task를 실행할 때 큐가 raw thread(원시 쓰레드)보다 효율적이지만, 블록을 만들고 큐에서 실행하는데 여전히 오버헤드가 있습니다. 블록의 작업량이 너무 적으면 큐에 보내는 것 보다 inline으로 실행하는 것이 더 저렴(cheap) 할 수 있습니다. 블록이 너무 적은 작업을 하고 있는지 확인하는 방법은 performance tools을 사용하여 각 경로(path)에 대한 메트릭을 수집하고 이를 비교하는것입니다.


● 기본 쓰레드와 관련된 데이터를 캐시하지말고, 다른 블록에서 해당 데이터에 액세스 할 수 있을것으로 예상하세요. 같은 큐에 있는 task가 데이터를 공유해야 하는 경우, dispatch queue의 컨텍스트 포인터를 사용하여 대신 데이터를 저장하세요. 


● 블록이 Objc객체를 몇개 이상 생성하는 경우, 블록의 코드 부분을 @autorelease 블록으로 묶어 해당 객체의 메모리 관리를 처리할 수 있습니다. 

GCD dispatch queues는 자체 autorelease pools을 가지고 있지만, pool이 비었을 때는 어떠한 보장도 하지 않습니다. 앱에 메모리가 제한되어 있으면, autorelease pool생성하여 일정한 간격으로 autorelease된 객체의 메모리를 확보할 수 있습니다. 


Creating and Managing Dispatch Queues

task를 큐에 추가하기 전에 사용할 큐의 타입과 사용방법을 결정해야합니다. Dispatch queues는  task를 순차적으로 또는 동시에 실행할 수 있습니다. 또한 큐를 염두에 두고, 특정 용도로 사용하는 경우, 이에 따라 큐의 속성을 구성 할 수도 있습니다. 다음 섹션에서는 Dispatch queues를 생성하고 사용하도록 구성하는 방법을 보여줍니다.


Getting the Global Concurrent Dispatch Queues

concurrent dispatch queue는 병렬로 실행할 수 있는 여러 task가 있는 경우에 유용합니다. concurrent queue는 FIFO구조로 task를 큐에서 제외한다는 점에서 여전히 큐입니다. 그러나 concurrent queue는 이전 task가 완료되기전에 추가 task를 큐에서 제거할 수 있습니다. 특정 순간에 concurrent queue에 의해 실행되는 실제 task수는 가변적이며, 앱의 조건이 변경되면 동적으로 변경 될 수 있습니다. 사용가능한 코어 수, 다른 프로세스가 수행하는 작업량, 다른 Serial task queue의 task 수 및 Priority 등 concurrent queue에 의해 실행되는 task수에 영향을 주는 요소가 많습니다.

시스템은 각 앱에 4개의 concurrent dispatch queues를 제공합니다. 이 큐는 앱에 대해 전역(global)이며, priority수준에 의해서만 구분됩니다. 이것들은 전역적이기 때문에 명시적으로 생성하지 않습니다. 대신 다음 예와 같이 dispatch_get_global_queue 함수를 사용하여 대기열 중 하나를 요청합니다.

점점...점점 내가 이걸 지금 공부해도 될까 라는 생각이 계속 드네요. 아예 차라리 Swift로 잘 정리된 GCD를 보는게 좋을 지도 ㅎ

암튼 위 코드는 deprecate됐고



안되겠습니당 남은 문서를 쫙 읽어보니까 그냥 단순한 Dispatch queue사용법이네요. 어차피 거의 모든 문서가 Objc기반이고, 아무렇지도 않긴한데 문서를 읽는 이유는 단순한 사용법이 아니라 뭐라해야되지..정말 원론적인걸 알고싶어서 읽는건데 Operation에는 그런게 진짜 많아서 많이 공부가 됐었는데..그러니까 남은 문서는 "너가 이렇게 하고싶어? -> Objc에 이런 메소드가 있음(심지어 원래 누르면 Swift버전도 보여주는데 그런거 없고 딱 Objc용)” 딱 이래가지고..암튼 계속 읽어도 공부가 안되는 느낌이라 계속 할 수가 없음 ㅋㄷㅋㄷ





반응형