티스토리 뷰

iOS 일반/Apple Guide

Thread Management

찜토끼 2017. 8. 17. 10:30

Apple 에서 제공하는 프로그래밍 가이드 중 Threading Programming Guide의 Thread Management 챕터를 공부하며 기록용으로 정리한 글입니다. 전문을 번역한 글이 아닌 점 참고바랍니다.



들어가며

OS X 또는 iOS 의 각 프로세스(어플리케이션)는 하나 또는 그 이상의 쓰레드로 구성됩니다. 이때 하나의 쓰레드는 코드를 통해 실행되는 하나의 경로 (single path of execution through the application's code) 를 나타냅니다. 모든 어플리케이션은 메인 함수를 실행하는 싱글 쓰레드로 시작합니다. 어플리케이션은 특별한 역할의 코드를 실행할 추가적인 쓰레드들도 생성할 수 있습니다.

어플리케이션이 새로운 쓰레드를 생성했을 때, 그 쓰레드는 어플리케이션의 프로세스 공간에서 독립적인 개체가 됩니다. 각 쓰레드는 각각의 실행 스택 (execution stack) 을 가지며, 각각 커널에 의해 런타임에 스케줄됩니다. 쓰레드는 다른 쓰레드나 다른 프로세스와 통신할 수 있고, I/O 오퍼레이션을 수행할 수 있으며, 그 외 당신이 필요로 할 만한 것들을 할 수 있습니다. 쓰레드들은 같은 프로세스 공간 안에 있지만, 하나의 어플리케이션이 사용하는 모든 쓰레드들은 같은 가상 메모리 공간을 공유하고 프로세스 자체에 대한 엑세스 권한도 똑같이 가집니다.

이 챕터에서는 OS X 와 iOS 에서 사용가능한 쓰레드 기술을 당신의 어플리케이션에서 어떻게 적용할 수 있는지 예를 들며 간략히 살펴보겠습니다.

<Note> Mac OS 의 쓰레드 아키텍처에 대한 히스토리와 쓰레드에 대한 추가적인 정보가 필요하면 Technical Note TN2028, “Threading Architectures” 를 보세요.


Thread Costs

쓰레드를 사용하는 것은 메모리 사용과 성능 면에서 당신의 프로그램에 (그리고 시스템에) real cost 를 가집니다. 각각의 쓰레드는 커널의 메모리 공간과 당신의 프로그램 메모리 공간 둘 다를 필요로 합니다. 쓰레드들을 관리하고 그것들의 스케줄링을 조정하는 역할을 하는 코어 스트럭처는 wired memory를 이용해 커널에 저장됩니다. 쓰레드들의 스택 공간과 각 쓰레드의 데이터들은 프로그램의 메모리에 저장됩니다. 이 스트럭처의 대부분은 쓰레드를 처음 생성할 때 생성되고 초기화됩니다. 따라서 이 프로세스는 비교적 비용이 많이 들 수 있습니다. 커널과의 인터랙션이 이루어지기 때문입니다.

당신의 어플리케이션에서 유저 레벨의 쓰레드를 새로 생성할 때 드는 대략적인 cost 를 다음 테이블에서 확인해보세요. 이중 몇 개는 설정을 통해 변경이 가능합니다.(ex: 보조 쓰레드의 스택 공간 양을 조절함으로써 cost를 변경) 쓰레드 생성에 있어서의 Time cost 는 어림짐작한 값이며 상대비교를 할 때만 참고하세요. 쓰레드 생성 시간은 프로세서 로드, 컴퓨터 속도, 시스템과 프로그램의 사용가능한 메모리에 따라 크게 좌우됩니다.

 Item

 Approximate cost

 Notes

Kernel data structures

Approximately 1 KB 

이 메모리는 쓰레드 데이터 스트럭처와 속성값들을 저장하는 데에 사용됩니다. 많은 부분이 wired memory 로 할당되기 때문에 디스크에 페이징 될 수 없습니다.

Stack space

512 KB (secondary threads)

8 MB (OS X main thread)

1 MB (iOS main thread)

보조쓰레드의 최소 스택 사이즈는 16KB이며 스택 사이즈는 4KB의 배수여야 합니다. 이 메모리의 공간은 쓰레드 생성 시기에 당신의 프로세스에서 따로 설정되지만, 이 메모리와 연관된 실제 페이지들은 그것이 필요한 시점에서 비로소 생성됩니다.

Creation time

Approximately 90 microseconds

이 값은 쓰레드 생성을 위한 최초 call에서부터 쓰레드의 entry point routine이 실행을 시작할 때까지 걸리는 시간을 반영합니다. 평균값과 중간값을 분석하여 결정한 값입니다.


<Note> Operation 객체들은 커널의 지원 하에 있기 때문에 가끔 쓰레드를 더 빠르게 생성할 수 있습니다. Operation 객체들은 쓰레드를 생판 처음부터 생성하기 보다는, allocation time 을 절약하기 위해 이미 커널에 있는 쓰레드 풀을 이용합니다. Operation 객체 사용에 대한 더 자세한 정보는 Concurrency Programming Guide 를 참고하세요.

쓰레드 코드를 작성할 때 고려해야할 또 하나의 cost 는 바로 생산 비용(production cost) 입니다. 쓰레드를 사용하는 어플리케이션을 설계하다 보면 때때로 어플리케이션의 데이터 스트럭처를 구성하는 방식을 근본적으로 바꿔야 할 일이 생깁니다. synchronization 의 사용을 피하기 위함입니다. synchronization 은 제대로 설계되지 않은 어플리케이션에서는 그 자체로 엄청난 성능적 패널티가 됩니다. 쓰레드를 고려해 데이터 스트럭처를 설계하고 쓰레드 코드에 대한 디버깅을 하는 것은 개발 시간을 많이 잡아먹을지도 모릅니다. 그러나 이런 과정을 건너뛰어 버린다면 런타임에 더 큰 문제를 야기할 수 있습니다.


Creating a Thread

Low-level 쓰레드를 생성하는 것은 비교적 간단합니다. 모든 케이스에서 당신은 반드시 쓰레드의 메인 진입점이 되어줄 함수나 메서드를 만들어야 하며, 쓰레드를 시작하기 위해서 사용가능한 쓰레드 루틴 중 하나를 사용해야 합니다. 일반적으로 많이 사용되는 쓰레드 생성 프로세스의 종류를 간략히 살펴보겠습니다. 이 부분은 자세히 정리하지 않고 넘어가니 필요하시면 원문을 참고바랍니다.


Using NSThread

클래스 메서드인 detachNewThreadSelector:toTarget:withObject: 를 사용하거나, NSThread 객체를 하나 생성하고 start 메서드를 호출하여 detached 쓰레드를 생성할 수 있습니다. (Detached 쓰레드 : 쓰레드가 종료될 때 쓰레드의 자원이 자동으로 시스템에 반납되는 쓰레드)

쓰레드가 Running 중인 NSThread 객체에 메시지를 전달하고 싶다면  performSelector:onThread:withObject:waitUntilDone: 메서드를 이용하세요.


Using POSIX Threads

OS X 와 iOS 는 C 기반의 POSIX Thread API 를 지원합니다. POSIX Thread 기술은 Cocoa 와 Cocoa Touch를 포함해 어떤 타입의 어플리케이션에도 사용될 수 있기 때문에, 멀티 플랫폼을 위한 소프트웨어를 만들 때 유용할 것입니다. pthread_create 를 호출하여 쓰레드를 생성하세요.


Using NSObject to Spawn a Thread

iOS와 OS X v10.5 이후 버전에서 모든 객체는 새로운 쓰레드를 생성할 수 있으며 자신의 메서드 중 하나를 실행시키기 위해 그 쓰레드를 사용할 수 있습니다. performSelectorInBackground:withObject: 메서드는 새로운 detached 쓰레드를 생성하며, 그 새로운 쓰레드의 진입점으로써 특정한 메서드를 사용합니다. 예를들면 만약 어떤 객체(myObj라고 이름 붙임)가 백그라운드 쓰레드에서 수행되어야 하는 어떤 메서드(doSomething이라고 이름 붙임)를 가지고 있다면 다음과 같이 코드를 작성해볼 수 있습니다.

[myObj performSelectorInBackground:@selector(doSomething) withObject:nil];


이 메서드를 호출하는 효과는 NSThread의 메서드인 detachNewThreadSelector:toTarget:withObject: 를 호출하는 것과 동일합니다. 새로운 쓰레드는 디폴트 환경설정값을 가지고 즉시 생성되며 running 됩니다. Selector 내부에서는 반드시 쓰레드에 대한 대응을 해야 합니다. 예를들면 오토릴리즈 풀을 준비해야 하고(가비지 컬렉션을 쓰지 않을 경우) 쓰레드의 런루프를 설정해야 합니다(사용할 계획이 있을 때). 새로운 쓰레드를 어떻게 설정하는지에 대한 정보는 Configuring Thread Attributes를 참조하세요.


Using POSIX Threads in a Cocoa Application

Cocoa 어플리케이션에서 쓰레드를 생성하는 주 인터페이스는 NSThread 입니다. 그러나 POSIX 쓰레드를 사용하는 것이 더 편한 사람은 POSIX 쓰레드를 사용할 수도 있습니다. 단, 몇 가지 주의사항이 있으니 가이드를 참고해서 코드를 작성하세요. (이 포스트에서는 생략합니다)


Configuring Thread Attributes

쓰레드를 생성한 후에, 혹은 그 전에, 쓰레드 환경을 부분부분 설정할 수 있습니다. 이 섹션에서는 당신이 변경할 수 있는 환경 설정들에 대해 알아보겠습니다.


Configuring the Stack Size of a Thread

쓰레드가 생성되면 시스템은 당신의 프로세스 공간에서 특정한 양의 메모리를 할당해줍니다. 해당 쓰레드를 위한 스택(Stack)이 동작할 수 있게요. 이 스택은 스택 프레임을 관리해줍니다. 또한 쓰레드를 위해 선언된 로컬 변수들이 저장되는 공간이기도 합니다. 쓰레드를 위해 할당되는 메모리 양은 앞서 Thread Costs 섹션에서 알아보았습니다.

만약 쓰레드에게 주어진 스택 사이즈를 변경하고 싶다면 쓰레드를 생성하기 전에 해야 합니다. 모든 쓰레딩 기술은 스택 사이즈를 설정할 방법을 제공합니다. (NSThread의 스택 사이즈 변경은 오직 iOS와 OS X v10.5 이상부터 가능하지만요) 각 쓰레딩 기술이 제공하는 옵션을 다음 표에 나타냈습니다.

Technology

Option

Cocoa

NSThread 객체를 할당하고 초기화하세요(detachNewThreadSelector:toTarget:withObject: 메서드를 사용하지 마세요). 쓰레드 객체의 start 메서드를 호출하기 전  setStackSize: 메서드를 사용하여 스택 사이즈를 변경하세요.

POSIX

새로운 pthread_attr_t 스트럭처를 생성하고 pthread_attr_setstacksize 함수를 사용하여 스택사이즈를 변경하세요. 쓰레드를 생성할 때 pthread_create 함수에 이 attributes 스트럭처를 함께 넘기세요.

Multiprocessing Services

쓰레드를 생성할 때 MPCreateTask 함수에 스택 사이즈 값을 함께 넘기세요.



Configuring Thread-Local Storage

각 쓰레드는 쓰레드 안 어디에서도 접근 가능한 딕셔너리를 가지고 있습니다. 이 딕셔너리는 쓰레드가 실행되는 동안 필요한 정보를 저장하는 데 사용됩니다. 예를 들면 쓰레드 런루프가 여러 번 반복되는 동안 지속하고 싶은 상태 정보를 저장할 때 사용할 수 있습니다.

Cocoa와 POSIX는 다른 방식으로 쓰레드 딕셔너리를 저장합니다. Cocoa에서는 NSThread 객체의 threadDictionary 메서드를 사용하여 NSMutableDictionary 객체를 얻을 수 있으며 여기에 key/value를 set/get 할 수 있습니다. POSIX에서는 pthread_setspecific 과 pthread_getspecific 함수를 사용하여 key/value를 set/get 할 수 있습니다.


Setting the Detached State of a Thread

가장 high-level의 쓰레드 기술은 detached 쓰레드를 디폴트로 생성합니다. 대부분의 경우에는 detached 쓰레드를 사용하는 것이 바람직합니다. detached 쓰레드는 완료하자마자 자신의 자원을 시스템에 돌려주기 때문입니다. 또한 detached 쓰레드는 당신의 프로그램과 명시적인 인터랙션을 할 필요가 없습니다. 쓰레드의 실행 결과를 얻어오는 것은 당신의 재량에 달려있습니다. 그에 비해 joinable thread의 경우에는 다른 쓰레드가 명시적으로 그 쓰레드와 join 하기 전까지는 자동으로 리소스를 반납하지 않습니다(쓰레드가 종료되기 전까지는 join을 수행한 쓰레드가 block됩니다).

Joinable 쓰레드는 child 쓰레드와 유사하다고 생각하면 됩니다. Joinable 쓰레드는 독립적인 쓰레드로써 돌지만, 시스템이 자원 회수를 요청하기 전에 반드시 다른 쓰레드에 의해 join 되어야 합니다. Joinable 쓰레드는 하나의 쓰레드에서 다른 쓰레드로 데이터를 넘길 명시적인 방법을 제공합니다. 종료되기 전에 데이터 포인터나 리턴값 등을 pthread_exit 함수를 통해 전달할 수 있습니다. 그러면 다른 쓰레드는 이 데이터를 pthread_join 함수를 통해 가져올 수 있습니다.

Important: 어플리케이션 종료 타이밍에 detached 쓰레드는 즉각 종료될 수 있지만 joinable 쓰레드는 그러지 못합니다. 각 joinable 쓰레드들은 프로세스가 종료되기 전에 join 되어야 합니다. 따라서 데이터를 디스크에 저장하는 등의 중요한 일들(도중에 멈추지 않아야 할 일들)에는 Joinable 쓰레드가 더 적합할지도 모릅니다.


Joinable 쓰레드를 생성하고 싶다면 POSIX 쓰레드를 사용하세요. POSIX는 디폴트로 joinable 쓰레드를 생성해줍니다. pthread_attr_setdetachstate 함수를 사용하여 detached 쓰레드로 만들 수도 있습니다. joinable 쓰레드가 실행된 후에는 pthread_detach 함수를 통해 detached 쓰레드로 변경할 수 있습니다.


Setting the Thread Priority

새롭게 생성되는 쓰레드는 디폴트 우선순위(priority)를 가집니다. 커널의 스케줄링 알고리즘은 쓰레드들을 실행시킬 때 이 우선순위를 염두에 둡니다. 그러나 쓰레드가 높은 우선순위를 가졌다고 해서 특정한 실행 시간을 보장받는 것은 아닙니다. 단지 스케줄러에 의해 선택될 가능성이 좀 더 높은 것 뿐입니다.

Important: 쓰레드의 우선순위를 디폴트로 유지하는 것이 일반적으로 좋습니다. 특정 쓰레드의 우선순위를 높이게 되면, 우선순위가 낮은 쓰레드들에서는 starvation이 발생할 가능성도 높아지기 때문입니다. 만약 당신의 어플리케이션이 우선순위가 높은 쓰레드와 낮은 쓰레드를 사용하며 이들 간의 인터랙션이 필요한 상황이라면, 우선순위가 낮은 쓰레드는 다른 쓰레드를 block 하며 병목 현상을 발생시킬지도 모릅니다.


만약 쓰레드의 우선순위를 변경하고 싶다면 Cocoa 쓰레드의 경우에는 NSThreadsetThreadPriority: 클래스 메서드를 사용하고, POSIX 쓰레드의 경우에는 pthread_setschedparam 함수를 사용하세요. 더 많은 정보가 필요하다면 NSThread 클래스 레퍼런스와 pthread_setschedparam 페이지를 참조하세요.


Writing Your Thread Entry Routine

당신이 만든 쓰레드의 진입점(entry point) 루틴의 구조는 OS X에서도 다른 플랫폼들과 마찬가지입니다. 데이터 스트럭처를 생성 및 초기화하고, 특정한 작업을 시키고(필요 시 런 루프를 셋업하고), 작업이 끝날 시 clean up을 해주면 됩니다. 어떻게 설계했는지에 따라 몇 가지 단계가 추가될 수도 있습니다.


Creating an Autorelease Pool

Objective-C 프레임워크를 사용하는 어플리케이션은 일반적으로 쓰레드 하나 당 적어도 한 개의 autorelease pool 을 반드시 생성합니다. 어플리케이션이 managed 모델(어플리케이션이 객체들의 retaining과 releasing을 관리함)을 사용한다면 autorelease pool은 쓰레드로부터 autoreleased 된 객체들을 잡아(catch)줍니다.

만약 어플리케이션이 managed memory model 대신 garbage collection을 사용한다면 autorelease pool의 생성은 필요하지 않습니다. 이 경우 autorelease pool은 단순히 무시될 것입니다. garbage collection과 managed memory model 둘 다 사용하는 어플리케이션에서는 autorelease pool을 반드시 사용해야 합니다. 이 경우 garbage collection이 활성화되는 순간에는 autorelease pool은 단순히 무시됩니다.

만약 당신의 어플리케이션이 managed memory model을 사용한다면, 당신이 쓰레드의 entry routine에서 맨 처음 해야할 일은 autorelease pool을 생성하는 일입니다. 그리고 이 autorelease pool을 없애는(destory) 일이 쓰레드에서 해야 할 맨 나중의 일입니다. Autorelease pool은 autoreleased 객체들을 잡아(catch)주는 것을 보장합니다. 쓰레드 자체가 종료되기 전까지는 이 객체들을 실제 release 시키지 않을지라도요. 다음 코드는 autorelease pool을 사용하는 간단한 쓰레드 entry routine의 구조입니다.

- (void)myThreadMainRoutine
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level pool 

    // Do thread work here.
    [pool release];  // Release the objects in the pool.
}

Top-level의 autorelease pool은 쓰레드가 종료될 때까지 객체들을 release 시키지 않으므로, 수명이 긴 쓰레드에서는 추가적인 autorelease pool을 생성하여 객체들을 좀 더 자주 release 시키도록 하는 것이 좋습니다. 예를 들어 run loop를 쓰는 쓰레드라면 run loop를 거칠 때마다 autorelease pool을 생성하고 해제(release)하는 방식입니다. 이렇게 객체들을 자주 release 하는 것은 어플리케이션의 메모리가 과다하게 증가하지 않도록 도와줍니다. 성능 면에도 도움을 줄 것입니다. 실제 성능을 측정해가며 autorelease pool의 적용방식을 적절히 조정하도록 하세요.

메모리 관리와 autorelease pool에 대해 더 많은 정보가 필요하다면 Advanced Memory Management Programming Guide 를 참조하세요.


Setting Up an Exception Handler

만약 당신의 어플리케이션이 예외(exception)를 처리하고 있다면 당신의 쓰레드 코드도 예외 상황이 발생했을 때를 대비해야 합니다. 예외 처리는 그것이 발생한 지점에서 처리되는 것이 가장 좋긴 하지만, 쓰레드 내에서 발생한 예외를 잡는 것을 실패한다면 어플리케이션이 종료될 수도 있습니다. 쓰레드 entry routine에 final try/catch를 심어놓음으로써 알 수 없는 에러를 잡고 적절하게 응답하도록 하세요.

Xcode로 프로젝트를 진행할 때는 C++과 Objective-C의 예외처리 스타일 중 하나를 사용할 수 있습니다. Objective-C에서 어떻게 예외를 발생시키고 잡을 수 있는지는 Exception Programming Topics를 참조하세요.


Setting Up a Run Loop

분리된 쓰레드에서 따로 돌릴 코드를 작성할 때 당신에게는 두 가지 옵션이 주어집니다. 첫 번째 옵션은 하나의 긴 작업을 방해(interruption)없이 수행한 후에 종료되는 쓰레드를 위한 코드를 작성하는 것입니다. 두 번째 옵션은 쓰레드를 loop에 넣고 주기적으로 작업을 요청하는 것입니다. 첫 번째 옵션은 코드 상 특별히 추가해야 할 것이 없습니다. 단순히 쓰레드가 할 작업을 작성하면 됩니다. 그러나 두 번째 옵션의 경우에는 쓰레드의 run loop를 셋팅해야 합니다.

OS X와 iOS는 모든 쓰레드에 대해 run loop 구현을 built-in으로 제공합니다. 앱 프레임워크에서는 자동으로 어플리케이션의 메인 쓰레드 run loop를 시작시킵니다. 만약 당신이 추가적인 쓰레드를 생성한다면 반드시 run loop를 설정하고 수동으로 시작시켜야 합니다.

run loop를 사용하고 설정하는 것에 대한 정보는 Run Loops를 참조하세요.


Terminating a Thread

쓰레드는 자신의 entry point routine 을 통해 정상적으로 종료되는 것이 좋습니다. 물론 Cocoa, POSIX, Multiprocessing Service는 쓰레드를 즉각 죽일(kill) routine들을 제공합니다. 그러나 이런 routine들은 굉장히 좋지 않습니다. 쓰레드를 죽이는 것은 쓰레드 자신의 clean up을 방해합니다. 이렇게 되면 쓰레드에게 할당된 메모리는 누수될(leak) 가능성이 생기고, 쓰레드에서 사용중이었던 다른 리소스들도 제대로 clean up되지 않아서 향후 문제를 발생시킬 수 있습니다.

만약 쓰레드가 실행 도중 종료되는 것을 원하지 않는다면, 설계 단계에서부터 쓰레드가 cancel 이나 exit 메시지에 응답할 수 있도록 해야 합니다. 작업 수행시간이 긴 경우, 작업을 주기적으로 멈추고 cancel 이나 exit 메시지가 도착했는지를 체크하는 방식일 것입니다. 만약 그런 메시지가 도착했다면(쓰레드를 exit하라고 요청하는 메시지) 쓰레드는 clean up을 제대로 수행하고 우아하게 종료하면 됩니다.

cancel 메시지에 응답하기 위한 하나의 방법을 소개합니다. run loop input source를 이용하는 방법입니다. 아래 코드는 쓰레드의 main entry routine 안에서 그것을 이용하는 예시입니다. (이 예시에서는 autorelease pool 이나 다른 셋팅 작업을 생략하였습니다.) 쓰레드 종료에 대한 조건을 thread dictionary의 key-value 쌍을 통해 주고받는 예시입니다. 코드 상의 주석에 각 스텝에 대한 설명이 친절히 되어있습니다.

- (void)threadMainRoutine
{
    BOOL moreWorkToDo = YES;
    BOOL exitNow = NO;
    NSRunLoop* runLoop = [NSRunLoop currentRunLoop]; 

    // Add the exitNow BOOL to the thread dictionary.
    NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary];
    [threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"]; 

    // Install an input source.
    [self myInstallCustomInputSource]; 

    while (moreWorkToDo && !exitNow)
    {
        // Do one chunk of a larger body of work here.
        // Change the value of the moreWorkToDo Boolean when done. 

        // Run the run loop but timeout immediately if the input source isn't waiting to fire.
        [runLoop runUntilDate:[NSDate date]];

         // Check to see if an input source handler changed the exitNow value.
       exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
    }
}


'iOS 일반 > Apple Guide' 카테고리의 다른 글

About Threaded Programming  (1) 2017.06.22
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함