Threading Programming Guide (2) - About Threaded Programming
많은 시간 동안 컴퓨터의 최대 성능은 컴퓨터 중심에 있는 싱글 마이크로프로세서의 속도에 의해 크게 제한이 되었다.
그러나 개별 프로세서의 속도가 한계에 도달하기 시작하면서 칩 제조업체는 멀티 코어 설계로 전환하여 컴퓨터가 여러 작업을 동시에 수행할 수 있는 기회를 제공했다.
OS X는 시스템 관련 작업을 수행을 할 때 멀티 코어를 활용한다. 애플리케이션도 스레드를 통해 멀티 코어를 활용할 수 있다.
스레드란 무엇일까?
스레드는 애플리케이션 내부에서 여러 실행 경로를 구현하는 비교적 가벼운 방법이다.
시스템 수준에서 프로그램은 나란히 실행되며 시스템은 각각의 프로그램의 요구에 따라 프로그램들의 실행 시간을 할당한다.
그러나 각각의 프로그램 내부에는 여러 작업을 동시에 또는 거의 동시에 수행하는 데 사용할 수 있는 하나 이상의 실행 스레드가 존재한다.
시스템은 실제로 이러한 실행 스레드를 관리하여 사용 가능한 코어에서 실행되도록 스케줄링하고 필요한 경우 다른 스레드가 실행될 수 있도록 미리 인터럽트 한다.
기술적인 관점에서 스레드는 코드 실행을 관리하는 데 필요한 커널 수준과 애플리케이션 수준의 데이터 구조체의 조합이다.
커널 수준의 구조체는 사용 가능한 코어 중 하나에 스레드에 대한 이벤트의 디스패치 및 스레드의 선제적 스케줄링을 조정한다.
애플리케이션 수준의 구조체에는 함수 호출을 저장하기 위한 호출 스택과 애플리케이션이 스레드의 속성과 상태를 관리하고 조작하는 데 필요한 구조체가 포함된다.
동시성을 지원하지 않는 애플리케이션(non-concurrent application)은 오직 하나의 스레드만 실행된다.
이 스레드는 애플리케이션의 메인 루틴으로 시작 및 종료되고 애플리케이션의 전체적인 동작을 구현하기 위해 다른 메서드나 함수로 하나씩 분기된다.
반면 동시성을 지원하는 애플리케이션은 하나의 스레드에서 시작하여 필요에 따라 추가 스레드를 생성한다.
각 스레드는 애플리케이션의 main 루틴에 있는 코드와 독립적으로 실행되는 고유한 사용자 정의 시작 루틴(own custom start routine)이 있다.
애플리케이션에 여러 개의 스레드가 있으면 다음과 같은 두 가지의 매우 중요한 잠재적 이점을 얻을 수 있다.
- 멀티 스레드는 애플리케이션의 응답성을 향상할 수 있다.
- 멀티 스레드는 멀티코어 시스템에서 애플리케이션의 실시간 성능을 향상할 수 있다.
애플리케이션에 스레드가 하나만 있는 경우 해당 스레드가 모든 작업을 수행해야 한다.
이벤트에 응답하고, 애플리케이션의 윈도우를 업데이트하고, 애플리케이션의 동작을 구현하는 데 필요한 모든 계산을 수행해야 한다.
스레드가 한 개일 때의 문제는 한 번에 하나의 작업만 수행할 수 있다는 것이다.
그렇다면 하나의 계산이 완료되는 데 오랜 시간이 걸린다면 어떻게 될까? 코드가 필요한 값을 계산하는 동안 애플리케이션은 사용자 이벤트에 응답하지 않고 윈도우를 업데이트하지 않는다. 계산하는데 오래 걸린다면 사용자는 애플리케이션이 중단되었다고 생각하고 강제로 종료하려고 할 수 있다.
그러나 사용자 정의 계산을 별도의 스레드로 이동하면 애플리케이션의 main 스레드는 사용자와 인터랙션에서 시기적절하게 응답할 수 있다.
오늘날 일반적으로 사용되는 멀티코어 컴퓨터의 경우 스레드는 일부 유형의 애플리케이션에서 성능을 향상할 수 있는 방법을 제공한다.
서로 다른 작업을 수행하는 스레드는 서로 다른 프로세서 코어에서 동시에 수행할 수 있으므로, 애플리케이션이 주어진 시간 동안 수행하는 작업의 양을 늘릴 수 있다.
물론 스레드는 애플리케이션의 성능 문제를 해결하기 위한 만병통치약은 아니다.
스레드가 제공하는 이점과 함께 잠재적인 문제가 발생한다.
애플리케이션에서 멀티 스레드를 사용하면 코드가 상당히 복잡해질 수 있다. 각 스레드는 애플리케이션의 상태 정보가 손상되지 않도록 다른 스레드와 작업을 조정해야 한다. 왜냐하면 하나의 애플리케이션 스레드들은 동일한 메모리 공간을 공유하기 때문에 동일한 데이터 구조체에 모두 액세스 할 수 있기 때문이다.
두 개의 스레드가 동일한 데이터 구조체를 동시에 조작하려고 하면 하나의 스레드가 다른 스레드의 변경 사항을 덮어쓰는 방식으로 결과가 손상될 수 있다.
적절한 보호 기능이 있더라도 코드 내에 미묘한(그다지 미묘하지 않은) 버그를 발생시키는 컴파일러 최적화에 주의해야 한다.
스레드 용어
스레드 및 지원 기술에 대한 설명에 들어가기 전에 몇 가지 기본 용어를 정의해보자.
UNIX 시스템에 익숙하다면 "작업"(Task)이라는 용어가 이 문서에서는 다르게 사용된다는 것을 알 수 있다. UNIX 시스템에서 "작업"이라는 용어는 실행 중인 프로세스를 나타내는 데 사용된다.
이 문서에는 다음 용어가 사용된다.
- 스레드(Thread)는 코드에 대한 별도의 실행 경로를 나타내는 데 사용한다.
- 프로세스(Process)는 여러 스레드를 포함할 수 있는 실행 중인 파일을 가리키는 데 사용한다.
- 작업(Task)은 수행되어야 할 작업의 추상적 개념을 가리키는 데 사용한다.
스레드의 대안
스레드를 직접 생성할 때 발생하는 한 가지 문제는 코드에 불확실성이 추가된다는 것이다.
스레드는 애플리케이션에서 동시성(concurrency)을 지원하는 비교적 낮은 수준의 복잡한 방법이다.
멀티 스레드가 미치는 영향을 완전히 이해하지 못할 경우, 동기화 또는 타이밍 문제가 쉽게 발생할 수 있다. 이러한 문제는 미묘한 동작 변화에서 애플리케이션의 손상 및 사용자 데이터의 손상까지 다양할 수 있다.
고려해야 할 또 다른 요소는 스레드 또는 동시성이 필요한지 여부이다.
스레드는 동일한 프로세스 내에서 동시에 여러 코드 경로를 실행하는 방법에 대한 특정 문제를 해결한다. 그러나 실행 중인 작업이 동시성을 보장하지 않는 경우가 있을 수 있다.
스레드는 메모리 소비량과 CPU 시간 측면에서 프로세스에 막대한 오버헤드를 초래한다. 이 오버헤드가 의도한 작업에 비해 너무 크거나 다른 옵션을 구현하기가 더 쉽다는 것을 생각할 수 있다.
하단의 표 1-1에서 스레드에 대한 몇 가지 대안을 나열했다. 이 표는 스레드에 대한 대체 기술(예를 들어 작업 객체와 GCD)과 이미 보유한 단일 스레드를 효율적으로 사용하도록 조정된 대안이 모두 포함되어 있다.
기술 | 설명 |
작업 객체(Operation Object) | OS X 10.5버전에 도입된 작업 객체는 일반적으로 보조 스레드에서 실행되는 작업의 래퍼(Wrapper)이다. 이 래퍼는 작업 수행의 스레드 관리 측면을 숨기므로 작업 자체에 집중 할 수 있다. 일반적으로 이러한 객체는 하나 이상의 스레드에서 작업 객체의 실행을 실제로 관리하는 작업 대기열 객체와 함께 사용된다. |
Grand Central Dispatch(GCD) | Mac OS X 10.6버전에 도입된 GCD는 스레드 관리보다는 수행해야 하는 작업에 집중할 수 있는 스레드의 또 다른 대안이다. GCD를 사용하면 수행할 작업을 정의하고 작업 대기열에 추가하여 적절한 스레드에서 작업 예약을 처리한다. 작업 대기열은 스레드를 사용하여 하는 작업을 보다 효율적으로 실행하기 위해 사용 가능한 코어 수와 현재 로드된 작업들을 고려해서 처리한다. |
유휴 시간 알림(Idle-time notifications) | 비교적 짧고 우선 순위가 매우 낮은 작업의 경우 유휴 시간 알림을 통해 애플리케이션이 사용 중이 아닐때 작업을 수행할 수 있다. Cocoa는 NSNotificationQueue 객체를 사용해 유휴 시간 알림을 지원한다. 유휴 시간 알림을 요청하려면 NSPostWhenIdle 옵션을 사용하여 default NSNotificationQueue 객체에 알림을 게시한다. 큐는 실행 루프(Run Loop)가 유휴 상태가 될 때까지 알림 객체의 전달을 지연시킨다. |
비동기 함수(Asynchronous functions) | 시스템 인터페이스에는 자동으로 동시성을 제공하는 많은 비동기 기능이 포함되어 있다. 이러한 API는 시스템 데몬 및 프로세스를 사용하거나 사용자 지정 스레드를 생성하여 작업을 수행하고 결과를 반환할 수 있다. (실제 구현은 코드와 분리되어 있기 때문에 무관하다.) 애플리케이션을 설계할 때 사용자 지정 스레드에서 동기 함수를 사용하는 대신 동일한 기능을 하는 비동기 함수를 찾아서 사용해보자. |
타이머(Timers) | 애플리케이션의 main 스레드에 타이머를 사용한다면 너무 단순해서 스레드가 필요하지 않지만 정기적인 서비스가 필요한 주기적인 작업을 수행할 수 있다. |
별도의 프로세스(Separate Processes) | 스레드보다 무겁지만 별도의 프로세스를 만드는 것은 작업이 애플리케이션과 직접적으로 관련되는 경우에만 유용할 수 있다. 작업에 상당한 양의 메모리가 필요하거나 루트 권한을 사용하여 실행해야 하는 경우 프로세스를 사용할 수 있다. 예를 들어 32비트 애플리케이션에서 사용자에게 결과를 표시하는 동안 64비트 서버 프로세스를 사용하여 대용량 데이터 셋을 계산할 수 있다. |
주의
fork 함수를 사용하여 별도의 프로세스를 시작할 때 항상 exec 또는 유사한 함수에 대한 호출을 한 후 fork 호출을 해야 한다.
Core Foundation, Cocoa 또는 Core Data 프레임워크(명시적이거나 암시적으로)에 의존하는 애플리케이션은 exec 함수에 대한 후속 호출을 수행해야 한다. 그렇지 않으면 해당 프레임워크가 부적절하게 작동할 수 있다.
스레드 지원
스레드를 사용하는 기존 코드가 있는 경우 OS X 및 iOS는 애플리케이션에서 스레드를 생성하기 위한 여러 기술이 제공된다.
또한 두 시스템 모두 이러한 스레드에서 수행해야 하는 작업을 관리하고 동기화할 수 있도록 지원한다.
스레드 패키지
스레드의 기본 구현 메커니즘이 Mach 스레드이지만, Mach 레벨에서 스레드를 사용하는 경우는 거의 없다.
대신, 보다 편리한 POSIX API 또는 파생 모델을 사용한다. 그러나 Mach 구현은 선점 실행 모델과 스레드가 서로 독립적 이도록 스케줄링할 수 있는 기능을 포함하여 모든 스레드의 기본 기능을 제공한다.
하단의 표 1-2에는 애플리케이션에서 사용할 수 있는 스레드 기술이 나열되어 있다.
기술 | 설명 |
Cocoa Threads | Cocoa는 NSThread 클래스를 사용하여 스레드를 구현한다. Cocoa는 또한 NSObject에서 새 스레드를 생성하고 이미 실행중인 스레드에서 코드를 실행하기 위한 메서드를 제공한다. |
POSIX Threads | POSIX 스레드는 스레드 생성을 위한 C 기반 인터페이스를 제공한다. Cocoa 애플리케이션을 작성하지 않은 경우 스레드 생성에 가장 적합한 선택이다. POSIX 인터페이스는 상대적으로 사용하기 쉽고 스레드 구성을 위한 충분한 유연성을 제공한다. |
Multiprocessing Services | 멀티프로세싱 서비스는 이전 버전의 Mac OS에서 전환하는 애플리케이션에서 사용하는 레거시 C 기반 인터페이스이다. 이 기술은 OS X에서만 사용할 수 있으며 새로운 개발에서는 피해야 한다. 대신 NSThread 클래스나 POSIX 스레드를 사용해야 한다. |
애플리케이션 수준에서 모든 스레드는 기본적으로 다른 플랫폼에서와 동일한 방식으로 작동한다.
스레드를 시작한 후 스레드는 실행 중(running), 준비(ready) 또는 차단(blocked)의 세 가지 주요 상태 중 하나로 실행이 된다.
스레드가 현재 실행되고 있지 않으면 차단되어 입력을 기다리고 있거나 실행할 준비가 되었지만 아직 실행하도록 스케줄링이 되지 않은 것이다. 스레드는 최종적으로 종료되고 종료된 상태로 이동할 때까지 세 가지 상태 사이에서 존재한다.
새로운 스레드를 만들 때 해당 스레드에 대한 entry-point function(또는 Cocoa Thread의 경우 entry-point method)을 지정해야 한다.
이 entry-point function은 스레드에서 실행하려는 코드를 구성한다.
함수가 반환되거나 스레드를 명시적으로 종료하면 스레드가 영구적으로 중지되고 시스템에서 회수된다.
스레드는 메모리와 시간 측면에서 생성하는 데 상대적으로 비용이 많이 들기 때문에 entry-point function이 상당한 양의 작업을 수행하거나 반복 작업을 수행할 수 있도록 실행 루프(run loop)를 설정하는 것이 좋다.
실행 루프(Run Loop)
실행 루프는 스레드에 비동기적으로 도착하는 이벤트를 관리하는 데 사용되는 인프라의 일부이다.
실행 푸르는 스레드에 대한 하나 이상의 이벤트 소스를 모니터링하여 작동한다.
이벤트가 도착하면 시스템은 스레드를 깨우고 이벤트를 실행 루프에 전송한 다음 지정한 처리기로 보낸다.
이벤트가 없고 처리할 준비가 되면 실행 루프는 스레드를 절전 모드로 전환한다.
생성한 스레드와 함께 실행 루프를 사용할 필요는 없지만 사용한다면 사용자에게 더 나은 경험을 제공할 수 있다. 실행 루프를 사용하면 최소한의 리소스를 사용하는 수명이 긴 스레드를 만들 수 있다. 실행 루프는 할 일이 없을 때 스레드를 절전 모드로 전환하기 때문에 폴링(polling) 할 필요가 없으므로 CPU 사이클을 낭비하거나 프로세서 자체가 절전되는 것을 방지한다.
실행 루프를 구성하려면 스레드를 시작하고, 실행 루프 객체에 대한 참조를 가져오고, 이벤트 핸들러를 설치하고, 실행 루프를 실행하도록 지시하기만 하면 된다. OS X에서 제공하는 인프라는 사용자를 위한 main 스레드의 실행 루프 구성을 자동으로 처리한다. 그러나 오래 지속되는 보조 스레드를 만들 계획이라면 해당 스레드에 대한 실행 루프를 직접 구성해야 한다.
동기화 도구(Synchronization Tools)
스레드 프로그래밍의 위험 중 하나는 여러 스레드 간의 리소스 경쟁이다.
여러 스레드가 같은 리소스를 동시에 사용하거나 수정하려고 하면 문제가 발생할 수 있다.
문제를 완화하는 한 가지 방법은 공유 리소스를 완전히 제거하고 각 스레드에 고유한 리소스 집합이 있는지 확인하는 것이다.
그러나 완전히 분리된 리소스를 유지하는 것이 옵션이 아닌 경우 잠금(lock), 조건(condition), 원자성 작업(atomic operation) 및 기타 기술을 사용하여 리소스에 대한 액세스를 동기화해야 한다.
잠금은 한 번에 하나의 스레드에서만 실행할 수 있는 코드에 대한 brute force 보호 기능을 제공한다.
가장 일반적인 잠금 유형은 뮤텍스(Mutex)라고도 하는 상호 배제 잠금이다.
스레드가 현재 다른 스레드가 보유하고 있는 뮤텍스를 얻으려고 하면 해당 스레드가 잠금을 해제할 때까지 차단이 된다.
여러 시스템 프레임워크는 모두 동일한 기본 기술을 기반으로 하지만 뮤텍스 잠금에 대한 지원을 제공한다.
또한, Cocoa는 재귀와 같은 다양한 유형의 행동을 지원하기 위해 여러 종류의 뮤텍스 잠금장치를 제공한다.
잠금 외에도 시스템은 조건에 대한 지원을 제공하여 애플리케이션 내에서 작업의 적절한 순서를 보장한다.
조건은 게이트 키퍼(gatekeeper) 역할을 하여 해당 조건이 참이 될 때까지 주어진 스레드를 차단한다. 참이 될 경우 조건은 스레드를 해제하고 계속할 수 있도록 한다.
POSIX계층과 Foundation 프레임워크는 모두 조건을 직접 지원한다.(작업 객체를 사용할 경우 작업 객체 간의 종속성을 구성하여 작업 실행 순서를 지정할 수 있으며, 이는 조건이 제공하는 동작과 매우 유사하다.)
잠금과 조건은 동시성 설계에서 매우 일반적이지만 원자성 작업은 데이터에 대한 액세스를 보호하고 동기화하는 또 다른 방법이다.
원자성 작업은 스칼라 데이터 유형에 대해 수학적 또는 논리적 연산을 수행할 수 있는 상황에서 잠금보다 가볍게 사용될 수 있는 대안을 제공한다.
원자성 작업에서는 특수 하드웨어 명령을 사용하여 다른 스레드가 변수에 액세스하기 전에 변수에 대한 수정이 완료되도록 한다.
스레드 간 통신(Inter-thread Communication)
우수한 설계로 필요한 통신량을 최소화하더라도 어느 시점에서는 스레드 간 통신이 필요하게 된다. (스레드의 역할은 애플리케이션을 위해 수행하는 것이지만, 해당 작업의 결과가 사용되지 않을 경우 어떠한 이점이 있을까?) 스레드는 새 작업 요청을 처리하거나 진행 상황을 애플리케이션의 main 스레드에 전달해야 할 수 있다.
이러한 상황에서는 한 스레드에서 다른 스레드로 정보를 전달하는 방법이 필요하다. 다행히도 스레드가 동일한 프로세스 공간을 공유한다는 사실은 통신을 위한 많은 옵션이 있음을 의미한다.
스레드 간 통신하는 방법은 여러가지가 있고 각각 고유한 장점과 단점이 있다.
Thread-Local Storage의 구성에는 OS X에서 사용할 수 있는 가장 일반적인 통신 메커니즘이 나열되어 있다. (메시지 대기열 및 Cocoa 분산 객체를 제외하고 이러한 기술은 iOS에서도 사용할 수 있다.)
하단의 표는 복잡성이 증가하는 순서대로 나열되어 있다.
매커니즘 | 설명 |
Direct Messaging | Cocoa 애플리케이션은 다른 스레드에서 직접 셀렉터를 수행할 수 있는 기능을 지원한다. 이 기능은 한 스레드가 기본적으로 다른 스레드에서 메서드를 실행할 수 있음을 의미한다. 대상 스레드의 컨텍스트(context)에서 실행되기 때문에 이러한 방식으로 전송 된 메시지는 해당 스레드에서 자동으로 직렬화가 된다. |
Global variables, shared memory, and objects | 두 스레드 간에 정보를 전달하는 또 다른 간단한 방법은 전역 변수, 공유 객체 또는 공유 메모리 블록을 사용하는 것이다. 공유 변수는 빠르고 간단하지만 직접 메시징보다 더 취약하다. 공유 변수는 코드의 정확성을 보장하기 위해 잠금 또는 기타 동기화 메커니즘으로 신중하게 보호되어야 한다. 그렇지 않으면 경쟁 상태, 데이터 손상 또는 충돌로 이어질 수 있다. |
Conditions | 조건은 스레드가 코드의 특정 부분을 싱행하는 시기를 제어하는 데 사용할 수 있는 동기화 도구이다. 조건을 게이트 키퍼로 생각하면 명시된 조건이 충족 될 때만 스레드가 실행된다. |
Run loop sources | 사용자 정의 실행 루프 소스는 스레드에서 애플리케이션 관련 메시지를 수신하도록 설정한 소스이다. 이벤트 기반이므로 실행 루프 소스는 할 일이 없을 때 스레드를 자동으로 절전 모드로 전환하여 스레드의 효율성을 향상시킨다. |
Ports and sockets | 포트 기반 통신은 두 스레드 간의 통신을 위한 보다 정교한 방법이지만 매우 안정적인 기술이기도 하다. 더 중요한 것은 포트와 소켓을 사용하여 다른 프로세스 및 서비스와 같은 외부 엔티티와 통신 할 수 있다는 것이다. 효율성을 위해 포트는 실행 루프 소스를 사용하여 구현되므로 포트에서 대기중인 데이터가 없을 때 스레드가 절전 모드로 전환된다. |
Message Queue | 레거시 멀티 프로세싱 서비스는 들어오고 나가는 데이터를 관리하기 위한 FIFO 대기열 추상화를 정의한다. 메시지 큐는 간단하고 편리하지만 다른 통신 기술만큼 효율적이지 않다. |
Cocoa distributed objects | 분산 오브젝트는 포트 기반 통신을 높은 수준으로 구현하는 Cocoa 기술이다. 비록 이 기술을 스레드 간 통신에 사용할 수 있지만, 그렇게 하는 것은 발생하는 오버헤드 때문에 매우 바람직하지 않다. 분산 오브젝트는 이미 프로세스 간에 이동하는 오버헤드가 높은 다른 프로세스와의 통신에 훨씬 적합하다. |
디자인 팁(Design Tips)
이번 섹션에서는 코드의 정확성을 보장하는 방식으로 스레드를 구현하는 데 도움이 되는 지침을 제공한다.
이러한 지침 중 일부는 자체 스레드 코드로 더 나은 성능을 달성하기 위한 팁도 제공한다. 다른 성능 팁과 마찬가지로 코드를 변경하기 전, 변경하는 동안 및 변경한 후에 항상 관련 성능 통계를 수집해야 한다.
명시적으로 스레드 생성 방지(Avoid Creating Threads Explicitly)
스레드 생성 코드를 수동으로 작성하는 것은 지루하고 오류가 발생할 가능성이 있으므로 가능한 한 피해야 한다. OS X 및 iOS는 다른 API를 통해 동시성을 암시적으로 지원한다.
스레드를 직접 생성하는 대신 비동기 API, GCD 또는 작업 객체를 사용하여 작업을 수행하는 것이 좋다.
이러한 기술은 백그라운드에서 스레드 관련 작업을 수행하며 올바르게 수행되도록 보장한다. 또한 GCD 및 작업 객체와 같은 기술은 현재 시스템 부하에 따라 활성 스레드 수를 조정하여 자신의 코드보다 훨씬 효율적으로 스레드를 관리하도록 설계되어있다.
합리적인 스레드 사용(Keep Your Threads Reasonably Busy)
스레드를 수동으로 만들고 관리하기로 결정한 경우 스레드가 귀중한 시스템 리소스를 소비한다는 것을 기억하라.
스레드에 할당한 작업이 합리적으로 오래 지속되고 생산적인지 확인하기 위해 최선을 다해야 한다. 동시에 대부분의 시간을 유휴 상태로 보내는 스레드를 종료하는 것을 두려워해서는 안된다.
스레드는 사소한 양의 메모리를 사용하며 일부는 시스템에 연결(wired)되어 있으므로 유휴 스레드를 해제하면 애플리케이션의 메모리 공간을 줄이는 데 도움이 될 뿐만 아니라 다른 시스템 프로세스에서 사용할 수 있도록 더 많은 물리적 메모리를 확보할 수 있다.
중요 : 유휴 스레드를 종료하기 전에 항상 애플리케이션의 현재 성능에 대한 일련의 기준 측정값을 기록해야 한다.
변경을 시도한 후 추가 측정을 수행하여 변경 사항이 성능을 손상시키는 것이 아닌 실제로 성능이 향상되고 있는지 확인해야 한다.
공유 데이터 구조 피하기(Avoid Shared Data Structures)
스레드 관련 리소스 충돌을 피하는 가장 간단하고 쉬운 방법은 프로그램의 각 스레드에 필요한 데이터의 자체 복사본을 제공하는 것이다.
병렬 코드(Parallel code)는 스레드 간의 통신 및 리소스 경쟁을 최소화할 때 가장 잘 작동한다.
멀티 스레드 애플리케이션을 만드는 것은 어렵다. 코드의 모든 올바른 지점에서 공유 데이터 구조를 잠그고 매우 주의하더라도 코드는 의미론적으로 안전하지 않을 수 있다.
예를 들어 공유 데이터 구조가 특정 순서로 수정될 것으로 예상할 경우 코드가 문제를 일으킬 수 있다. 그것을 방지하기 위해 코드를 트랜잭션 기반 모델로 변경하면 이후에 스레드가 여러 개 있을 경우의 성능 이점이 무효화될 수 있다. 처음에 리소스 경쟁을 제거하면 설계가 단순해지고 성능이 향상된다.
스레드와 사용자 인터페이스(Threads and Your User Interface)
애플리케이션에 그래픽 UI가 있는 경우 사용자 관련 이벤트를 수신하고 애플리케이션의 main 스레드에서 인터페이스 업데이트를 시작하는 것이 좋다.
이 방법은 사용자 이벤트 처리 및 윈도우 내용 그리기 작업과 관련된 동기화 문제를 방지하는 데 도움이 된다.
Cocoa와 같은 일부 프레임워크는 일반적으로 이러한 동작을 요구하지만, 그렇지 않은 프레임워크의 경우에도 이 동작을 main 스레드에서 유지하면 UI를 관리하기 위한 논리를 단순화할 수 있는 이점이 있다.
다른 스레드에서 그래픽 작업을 수행하는 것이 유리한 몇 가지 예외가 있다.
예를 들어 보조 스레드를 사용하여 이미지를 생성 및 처리하고 다른 이미지 관련 계산을 수행할 수 있다.
이러한 작업은 보조 스레드를 사용하면 성능을 크게 향상시킬 수 있다. 그러나 특정 그래픽 작업에 대해 확실하지 않은 경우 main 스레드에서 수행하도록 해라.
종료 시 스레드 동작 인식(Be Aware of Thread Behaviors at Quit Time)
프로세스는 모든 비연결 스레드(non-detached threads)가 종료될 때까지 실행된다.
기본적으로 애플리케이션의 main 스레드만 비연결 스레드로 작성되지만, 다른 스레드를 비연결 스레드로 만들 수 있다.
사용자가 애플리케이션을 종료할 때, 연결된 스레드(detached threads)에 의해 수행되는 작업은 선택사항으로 간주되기 때문에 모든 연결된 스레드를 즉시 종료하는 것이 일반적으로 적절한 동작으로 간주된다. 하지만 애플리케이션이 백그라운드 스레드를 사용하여 데이터를 디스크에 저장하거나 다른 중요한 작업을 수행하는 경우 해당 스레드를 비연결 스레드로 생성하여 애플리케이션 종료 시 데이터 손실을 방지할 수 있다.
비연결 스레드(조인 가능(joinable)이라고도 함)를 만들려면 추가 작업이 필요하다.
대부분의 고급 스레드 기술은 기본적으로 조인 가능한 스레드를 생성하지 않기 때문에 POSIX API를 사용하여 스레드를 생성해야 할 수 있다.
또한 애플리케이션의 main 스레드에 종료될 때 비연결 스레드와 조인하려면 추가적인 코드가 필요하다.
Cocoa 애플리케이션을 만드는 경우 applicationShouldTerminate 델리게이트 메서드를 사용하여 애플리케이션의 종료를 연기하거나 완전히 취소하는 방법도 있다.
종료를 연기하는 경우 프로그램은 중요한 스레드가 작업을 마칠 때까지 기다린 다음 replyToApplicationShouldTerminate 메서드를 호출해야 한다.
예외 처리(Handle Exceptions)
예외처리 메커니즘은 예외가 발생할 때 필요한 정리를 수행하기 위해 현재 호출 스택에 의존한다.
각 스레드는 자체 호출 스택을 가지기 때문에 각 스레드는 자체 예외를 확인할 책임이 있다.
보조 스레드에서 예외를 잡지 못하면 main 스레드에서 예외를 잡지 못하는 것과 같다. 이것의 의미는 프로세스가 종료된다는 의미이다.
예외처리를 위해 다른 스레드에 확인되지 않은 예외를 throw 할 수 없다.
현재 스레드에서 예외적인 상황을 다른 스레드(예를 들어 main 스레드)에 알려야 하는 경우 예외를 확인하고 발생한 상황을 나타내는 메시지를 다른 스레드로 보내야 한다.
모델과 수행하려는 작업에 따라 예외를 확인한 스레드가 처리를 계속하거나(가능한 경우), 지시를 기다리거나 단순히 종료할 수 있다.
참고: Cocoa에서 NSException 객체는 일단 확인된 후 스레드에서 다른 스레드로 전달될 수 있는 자체 포함 객체이다.
경우에 따라 예외 처리기가 자동으로 생성될 수 있다. 예를 들어 Objective-C의 @synchronized 지시문에는 암시적 예외 처리기를 포함되어 있다.
스레드를 깨끗하게 종료(Terminate Your Threads Cleanly)
스레드가 종료되는 가장 좋은 방법은 main entry point 루틴의 끝에 도달하는 것이다. 스레드를 즉시 종료하는 기능이 있지만 이러한 기능은 최후의 수단으로만 사용해야 한다. 스레드가 자연스러운 entry point에 도달하기 전에 스레드를 종료하면 스레드가 자체적으로 정리되지 않는다. 스레드가 메모리를 할당했거나 파일을 열거나 다른 유형의 리소스를 획득한 경우 코드에서 해당 리소스를 회수할 수 없어 메모리 누수 또는 기타 잠재적인 문제가 발생할 수 있다.
라이브러리의 스레드 안전성(Thread Safety in Libraries)
애플리케이션 개발자는 애플리케이션이 멀티 스레드로 실행되는지 여부를 제어할 수 있지만 라이브러리 개발자는 그렇지 않다.
라이브러리를 개발할 때 호출하는 애플리케이션이 멀티 스레드이거나 언제든지 멀티 스레드로 전환될 수 있다고 가정해야 한다.
따라서 중요한 코드 섹션에는 항상 잠금을 사용해야 한다.
라이브러리 개발자의 경우, 애플리케이션이 멀티 스레드가 될 때만 잠금을 만드는 것은 현명하지 않다.
어떤 시점에서 코드를 잠그려면 라이브러리 사용 초기에 잠금 객체를 생성하라.
라이브러리를 초기화할 수 있는 명시적 호출을 통해 잠금 객체를 만드는 것이 좋다.
정적 라이브러리 초기화 기능을 사용하여 이러한 잠금을 만들 수도 있지만 다른 방법이 없는 경우에만 이 기능을 사용하라.
초기화 기능을 실행하면 라이브러리를 로드하는 데 필요한 시간이 늘어나 성능에 부정적인 영향을 미칠 수 있다.
참고 : 라이브러리 내에서 뮤텍스 잠금에 대해 Lock과 Unlock을 동일하게 사용해야 한다. 또한 스레드 안전(thread-safe) 환경을 제공하기 위해 호출 코드에 의존하지 말고 라이브러리 데이터 구조를 잠가야 한다.
Cocoa 라이브러리를 개발 중인 경우 애플리케이션이 멀티 스레드 될 때 알림을 받고 싶다면 NSWillBecomeMultiThreadedNotification의 옵저버로 등록할 수 있다. 하지만 라이브러리 코드가 호출되기 전에 전달될 수 있으므로 이 알림을 받는 것에 의존해서는 안된다.
[원문]
About Threaded Programming
About Threaded Programming For many years, maximum computer performance was limited largely by the speed of a single microprocessor at the heart of the computer. As the speed of individual processors started reaching their practical limits, however, chip m
developer.apple.com