Thread Programming Guide - About Threaded Programming

업데이트:

Thread Programming Guide - About Threaded Programming 를 번역한 글입니다.

영어 실력이 많이 부족합니다ㅠ 수정 제안 환영합니다 🙌

수 년 동안 컴퓨터의 최대 성능은 컴퓨터의 심장인 단일 마이크로프로세서의 속도에 의해 크게 제한되었습니다. 그러나 개별 프로세서의 속도가 사실상 한계에 도달하기 시작하면서, 칩 제조업체들은 멀티코어 설계로 전환하여 컴퓨터가 여러 작업을 동시에 수행할 수 있는 기회를 제공했습니다. OS X는 시스템 관련 작업을 수행할 수 있을 때마다 이러한 코어를 활용하지만, 자체 앱(당신이 만드는)에서도 스레드를 통해 이러한 코어를 활용할 수 있습니다.

What Are Thread?

스레드는 앱 내부에서 여러 실행 경로를 구현하는 비교적 가벼운 방법입니다. 시스템 수준에서 프로그램은 나란히 실행되며, 시스템은 해당 프로그램의 필요와 다른 프로그램들의 필요에 따라 각 프로그램에 실행 시간을 할당합니다. 그러나 각 프로그램 내부에는 다른 작업을 동시에 또는 거의 동시에 수행하는 데 사용할 수 있는 하나 이상의 실행 스레드가 있습니다. 시스템은 실제로 이러한 실행 스레드를 자체적으로 관리하여 사용가능한 코어에서 실행되도록 스케줄링하고 다른 스레드가 실행될 수 있도록 필요에 따라 이를 선제적으로 중단합니다.

기술적인 관점에서, 스레드는 코드 실행을 관리하는 데 필요한 커널 수준 및 앱 수준 데이터 구조의 조합입니다. 커널 수준 구조는 스레드에 대한 이벤트 디스패치와 사용가능한 코어 중 하나에서 스레드의 선점 스케줄링을 조정합니다. 앱 수준 구조에는 함수 호출을 저장하기 위한 콜스택과 앱이 스레드의 속성과 상태를 관리하고 조작하는 데 필요한 구조가 포함됩니다.

동시 실행이 아닌 앱(Non-current Application)에는 실행 스레드가 하나만 존재합니다. 이 스레드는 앱의 메인 루틴으로 시작하고 끝나고 앱의 전체 동작을 구현하기 위해 다른 메서드나 함수로 하나씩 분기합니다. 대조적으로, 동시성을 지원하는 앱은 하나의 스레드로 시작하여 필요에 따라 추가 실행 경로를 생성하기 위해 더 추가합니다. 각각의 새 경로에는 앱의 메인 루틴에 있는 코드와 독립적으로 실행되는 고유한 커스텀 시작 루틴이 존재합니다. 앱에 여러 스레드가 있으면 매우 중요한 두가지 잠재적 이점을 가집니다:

  • 다중 스레드는 앱의 인지된 응답성(perceived responsiveness)를 향상시킬 수 있습니다.
  • 다중 스레드는 다중 코어 시스템에서 앱의 실시간 성능을 향상시킬 수 있습니다.

앱에 스레드가 하나만 존재하는 경우, 해당 스레드가 모든 작업을 수행해야 합니다. 이벤트에 응답하고, 앱의 윈도우를 업데이트하며, 앱의 동작을 구현하는데 필요한 모든 계산을 수행해야 합니다. 스레드가 하나만 존재하는 경우의 문제는 한 번에 한 가지 작업만 수행할 수 있다는 것입니다. 그렇다면 계산 중 하나를 완료하는 데 시간이 오래 걸린다면 어떻게 될까요? 코드가 필요한 값을 계산하느라 바쁜 동안 앱은 사용자 이벤트 응답과 윈도우 업데이트를 중지합니다. 이 동작이 오래 지속되면 사용자는 앱이 중단된 것으로 생각하고, 강제 종료를 시도할 수 있습니다. 그러나 커스텀 계산을 별도의 스레드로 이동하면, 앱의 메인 스레드가 보다 적절한 방식으로 사용자 인터랙션에 자유롭게 응답할 수 있습니다.

요즘 일반적인 멀티코어 컴퓨터에서는 스레드는 일부 유형의 앱에서 성능을 향상시키는 방법을 제공합니다. 다른 작업을 수행하는 스레드는 다른 프로세서 코어에서 동시에 작업을 수행할 수 있으므로 앱이 주어진 시간동안 수행하는 작업의 양을 늘릴 수 있습니다.

물론, 스레드가 앱의 성능 문제를 해결하기 위한 만병통치약은 아닙니다. 스레드가 제공하는 이점과 함께 잠재적인 문제도 따라옵니다. 앱에 여러 실행 경로가 있으면 코드에 상당한 복잡성이 추가될 수 있습니다. 각 스레드는 앱의 상태 정보가 손상되는 것을 방지하기 위해 다른 스레드와 동작(action)을 조정해야 합니다. 하나의 앱의 스레드들은 동일한 메모리 공간을 공유하기 때문에, 동일한 모든 데이터 구조에 접근할 수 있습니다. 두 스레드가 동시에 동일한 데이터 구조를 조작하려고 하면, 한 스레드가 데이터 구조 결과를 손상시키는 방식으로 다른 스레드의 변경사항을 덮어쓸 수 있습니다. 적절한 보호 조치를 취하더라도, 파악하기 어려운(그리고 그렇게 파악하기 어렵지 않은) 버그를 끼워넣을 수 있는 컴파일러 최적화를 조심해야 합니다.

Threading Terminology

스레드와 스레드 지원 기술에 대해 깊이 들어가기 전에 몇 가지 기본 용어를 정의할 필요가 있습니다.

만약 UNIX 시스템에 친숙하다면, 이 문서에서 “작업(task)”이라는 용어가 다르게 사용됨을 알 수 있습니다. UNIX 시스템에서 “작업”이라는 용어는 때때로 실행 중인 프로세스를 지칭하기 위해 사용됩니다.

이 문서는 다음 용어를 사용합니다:

  • 스레드라는 용어는 코드에 대한 별도의 실행 경로를 나타내는 데 사용됩니다.
  • 프로세스라는 용어는 여러 스레드를 포함할 수 있는 실행 중인 실행파일(executable)을 나타내는 데 사용됩니다.
  • 작업이라는 용어는 수행해야 하는 일(work)의 추상적 개념을 나타내는 데 사용됩니다.

Alternatives to Threads

스레드를 직접 생성할 때의 한 가지 문제는 코드에 불확실성을 추가한다는 것입니다. 스레드는 앱에서 동시성을 지원하는 상대적으로 낮은 수준의 복잡한 방법입니다. 설계 선택이 미치게 될 영향을 완전히 이해하지 못하면 동기화 또는 타이밍 문제가 쉽게 발생할 수 있으며, 그 심각도는 미묘한 동작 변경에서 앱 충돌 및 사용자 데이터 손상에 이르기까지 다양합니다.

고려해야 할 또 다른 요소는 스레드 또는 동시성이 필요한지 여부입니다. 스레드는 동일한 프로세스 내에서 여러 코드 경로를 동시에 실행하는 방법의 특정 문제를 해결합니다. 그러나 수행하는 일의 양이 동시성을 보장하지 않는 경우가 있을 수 있습니다. 스레드는 메모리 소비와 CPU 시간 측면에서 프로세스에 엄청난 양의 오버헤드를 발생시킵니다. 이 오버헤드가 의도한 작업에 대해 너무 크거나 다른 옵션이 구현하기 더 쉽다는 것을 발견할 수 있습니다.

표 1-1은 스레드에 대한 몇 가지 대안을 나열합니다. 이 표에는 스레드 대체 기술(예를 들어 operation 객체 및 GCD)과 이미 가지고 있는 단일 스레드를 효율적으로 사용하도록 설계된 대안이 모두 포함되어 있습니다.

표 1-1 스레드에 대한 대체 기술

기술 설명
Operation 객체 OS X v10.5에서 도입된 operation 객체는 일반적으로 보조 스레드에서 실행되는 작업에 대한 래퍼(wrapper)입니다. 이 래퍼는 작업 수행의 스레드 관리 측면을 숨기므로 작업 자체에 집중할 수 있습니다. 일반적으로 하나 이상의 스레드에서 operation 객체의 실행을 실제로 관리하는 operation queue와 함께 이 객체를 사용합니다.
operation 객체를 사용하는 방법에 대한 자세한 내용은 Concurrency Programming Guide을 참고하세요.
Grand Central Dispatch(GCD) Mac OS x v10.6에서 도입된 Grand Central Dispatch는 스레드 관리보다 수행해야하는 작업에 집중할 수 있도록하는 스레드의 또 다른 대안입니다. GCD를 사용하려면, 수행하려는 작업을 정의하고, 적절한 스레드에서 작업의 스케줄링을 처리하는 작업(work) queue에 추가합니다. 작업(work) queue는 사용가능한 코어 개수와 현재 로드를 고려하여 개발자보다 더 효율적으로 작업을 실행합니다.
GCD와 작업(work) queues를 사용하는 방법에 대한 자세한 내용은 Concurrency Programming Guide을 참고하세요.
유휴 시간 알림(Idle-time notifications) 상대적으로 짧고 우선순위가 매우 낮은 작업의 경우 유휴 시간 알림을 통해 앱이 바쁘지 않은 시간에 작업을 수행할 수 있습니다. 코코아는 NSNotificationQueue 객체를 사용해 유휴 시간 알림을 지원합니다. 유휴 시간 알림을 요청하려면 NSPostWhenIdle 옵션을 사용해 기본 NSNoticiationQueue 객체에 알림을 게시하세요. queue는 런루프가 유휴 상태가 될 때까지 알림 객체의 전달을 지연시킵니다. 자세한 내용은 Notification Programming Topics을 참고하세요.
비동기 함수 시스템 인터페이스에는 자동 동시성을 제공하는 많은 비동기 함수가 포함되어 있습니다. 이러한 API들은 시스템 데몬 및 프로세스를 사용하거나 커스텀 스레드를 생성하여 작업을 수행하고 결과를 반환할 수 있습니다. 앱을 디자인할 때, 비동기 동작을 제공하는 함수를 찾고, 커스텀 스레드에서 동등한 동기 함수를 사용하는 대신 사용하는 것을 고려하세요.
Timers 앱의 메인 스레드에서 타이머를 사용하여 스레드를 필요로 하기엔 너무 사소하지만 정기적인 간격으로 서비스해야하는 주기적 작업을 수행할 수 있습니다. 타이머에 대한 자세한 내용은 Timer Sources을 참고하세요.
프로세스 분리 스레드보다 더 무겁지만 작업이 앱과 약간 스칠 정도로만(tangentially) 관련된 경우, 별도의 프로세스를 만드는 것이 유용할 수 있습니다. 작업에 상당한 양의 메모리가 필요하거나 루트 권한을 사용해 실행해야하는 경우 프로세스를 사용할 수 있습니다. 예를 들어, 32비트 앱이 사용자에게 결과를 표시하는 동안 64비트 서버 프로세스를 사용해 대용량 데이터셋을 계산할 수 있습니다.

⚠️ Warning: fork 함수를 사용해 별도의 프로세스를 시작할 때, 항상 exec 또는 유사한 함수에 대한 호출과 함께 fork 호출을 따라야 합니다. Core Foundation, Cocoa 또는 Core Data 프레임워크(명시적이든 암시적이든)에 의존하는 앱은 exec 함수에 대한 후속 호출을 수행해야 하며 그렇지 않으면 해당 프레임워크가 부적절하게 작동할 수 있습니다.

Threading Support

스레드를 사용하는 기존 코드가 있는 경우, OS X 및 iOS는 앱에서 스레드를 생성하기 위한 여러 기술을 제공합니다. 또한 두 시스템 모두 해당 스레드에서 수행해야 하는 작업(work)의 관리 및 동기화를 지원합니다. 다음 섹션에서는 OS X 및 iOS에서 스레드로 작업할 때 알고 있어야 하는 몇 가지 핵심 기술에 대해 설명합니다.

Threading Packages

스레드의 기본 구현 메커니즘은 마하(Mach, 커널) 스레드이지만 마하 수준에서 스레드로 작업하는 경우는 거의 없습니다. 대신, 일반적으로 더 편리한 POSIX API 또는 파생상품 중 하나를 사용합니다. 마하 구현은 모든 스레드의 기본 기능을 제공하지만, 여기에는 선점형 실행 모델과 스레드가 서로 독립적이도록 스케줄링하는 기능이 포함됩니다.

표 1-2는 앱에서 사용할 수 있는 스레딩 기술을 나열합니다.

표 1-2 스레드 기술

기술 설명
코코아 스레드 코코아는 NSThread 클래스를 사용해 스레드를 구현합니다. 코코아는 또한 새 스레드를 생성하고 이미 실행 중인 스레드에서 코드를 실행하기 위해 NSObject에 메서드를 제공합니다. 자세한 내용은 Using NSThreadUsing NSObject to Spawn a Thread를 참고하세요.
POSIX 스레드 POSIX 스레드는 스레드 생성을 위한 C 기발 인터페이스를 제공합니다. 코코아 앱을 개발하지 않는 경우에 스레드를 생성하기 위한 최선의 선택입니다. POSIX 인터페이스는 사용이 비교적 간단하고 스레드 구성을 위한 충분한 유연성을 제공합니다. 자세한 내용은 Using POSIX Threads을 참고하세요.
멀티프로세싱 서비스 멀티프로세싱 서비스는 이전 버전의 Mac OS에서 전환하는 앱에서 사용되는 레거시 C 기반 인터페이스입니다. 이 기술은 OS X에서만 사용할 수 있으며 새로운 개발에서는 피해야 합니다. 대신 NSThread 클래스 또는 POSIX 스레드를 사용해야 합니다. 이 기술에 대해 더 많은 정보가 필요하다면 Multiprocessing Services Programming Guide를 참고하세요.

앱 수준에서 모든 스레드는 기본적으로 다른 플랫폼에서와 같은 방식으로 동작합니다. 스레드를 시작한 후, 스레드는 세 가지 주요 상태(running, ready, or blocked) 중 하나로 실행됩니다. 스레드가 현재 실행 중이 아니라면, 차단되어 입력을 기다리거나 실행할 준비가 되었지만 아직 실행되도록 스케줄되지 않은 것 입니다. 스레드는 마지막으로 종료되어 terminated 상태로 이동할 때까지 이러한 상태 사이에서 계속 앞뒤로 이동합니다.

새로운 스레드를 생성할 때, 해당 스레드에 대한 진입점 함수(또는 코코아 스레드의 경우 진입점 메서드)를 지정해야 합니다. 이 진입점 함수는 스레드에서 실행하려는 코드를 구성합니다. 함수가 반환되거나 스레드를 명시적으로 종료하면, 스레드가 영구적으로 중지되고 시스템에서 회수합니다. 스레드는 메모리와 시간 측면에서 생성하는데 상대적으로 비용이 많이 들기 때문에, 진입점 함수가 상당한 양의 작업(work)을 수행하거나 반복 작업(work)을 할 수 있도록 런루프를 설정하는 것이 좋습니다.

사용가능한 스레딩 기술과 사용방법에 대한 자세한 내용은 Thread Management를 참고하세요.

Run Loops

런 루프는 스레드에 비동기적으로 도착하는 이벤트를 관리하는 데 사용되는 인프라입니다. 런 루프는 스레드에 대한 하나 이상의 이벤트 소스를 모니터링하여 작동합니다. 이벤트가 도착하면, 시스템은 스레드를 깨우고 런 루프에 이벤트를 전달한 다음 지정한 핸들러에 이벤트를 전달합니다. 이벤트가 없고 처리할 준비가 된 경우, 런 루프는 스레드를 절전 모드로 전환합니다(puts the thread to sleep).

생성한 스레드에 런 루프를 사용할 필요는 없지만 런 루프를 사용하면 사용자에게 더 나은 경험을 제공할 수 있습니다. 런 루프를 사용하면 최소한의 리소스를 사용하면서 수명이 긴 스레드를 생성할 수 있습니다. 런 루프는 할 일이 없을 때 스레드를 절전모드로 전환하기 때문에 CPU 사이클을 낭비하는 폴링이 필요하지 않고, 프로세서가 절전모드로부터 자신을 지키고, 전원을 절약합니다.

런 루프를 구성하려면, 스레드를 시작하고, 런 루프 객체에 대한 참조를 얻고, 이벤트 핸들러를 설치하고, 런 루프가 실행되도록 지시하기만 하면 됩니다. OS X에서 제공하는 인프라는 자동으로 메인 스레드의 런 루프 구성을 처리합니다. 그러나 수명이 긴 보조 스레드를 생성할 계획이라면, 해당 스레드에 대한 런 루프를 직접 구성해야 합니다.

런 루프에 대한 자세한 내용과 사용 방법에 대한 예시는 Run Loops에서 제공됩니다.

Synchronization Tools

스레드 프로그래밍의 위험 요소 중 하나는 여러 스레드 간의 리소스 경쟁입니다. 여러 스레드가 동일한 리소스를 사용하거나 수정하려고 하면 문제가 발생할 수 있습니다. 문제를 완화하는 한 가지 방법은 공유 리소스를 완전히 제거하고 각 스레드에서 작동할 고유한 리소스 집합이 있는지 확인하는 것입니다. 완전히 별도의 리소스를 유지 관리할 수 없는 경우, lock, conditions, atomic operations 및 기타 기술을 사용해 리소스에 대한 접근을 동기화해야 할 수 있습니다.

잠금(Locks)은 한 번에 하나의 스레드에서만 실행할 수 있는 코드에 대한 무차별적인 형태의 보호(brute force form of protection)를 제공합니다. 가장 일반적인 유형의 잠금은 뮤텍스(mutex)라고도 하는 상호 배제 잠금입니다. 스레드가 현재 다른 스레드가 보유하고 있는 뮤텍스를 얻으려고 하면 다른 스레드가 잠금을 해제할 때까지 차단됩니다. 여러 시스템 프레임워크는 모두 동일한 기본 기술을 기반으로 하지만 뮤텍스 잠금을 지원합니다. 또한 코코아는 재귀와 같은 다양한 유형의 동작을 지원하기 위해 뮤텍스 잠금의 여러 변형을 제공합니다. 사용 가능한 타입에 대한 더 자세한 내용은 Locks을 참고하세요.

잠금 외에도 시스템은 앱 내에서 적절한 작업 순서를 보장하는 컨디션(conditions)에 대한 지원을 제공합니다. 컨디션(conditions)은 문지기(gatekeeper) 역할을 하며, 이를 나타내는 조건이 참이 될때까지 주어진 스레드를 차단합니다. 조건이 참이 되면, 컨디션은 스레드를 해제하고 계속하도록 허용합니다. POSIX 계층과 Foundation 프레임워크는 모두 컨디션에 대한 직접적인 지원을 제공합니다.(operation 객체를 사용하는 경우, 작업 실행을 순서대로 수행하도록 operation 객체 간의 종속성을 구성할 수 있으며, 이는 컨디션에서 제공하는 동작과 매우 유사합니다.)

잠금 및 컨디션은 동시성 설계에서 매우 일반적이지만 원자적(atomic) 작업은 데이터 접근을 보호하고 동기화하는 또 다른 방법입니다. 원자적 작업(Atomic operations)은 스칼라 데이터 타입에 대해 수학적 또는 논리적 연산을 수행할 수 있는 상황에서 잠금에 대한 간단한 대안을 제공합니다. 원자적 작업은 특별한 하드웨어 명령어를 사용해 다른 스레드가 변수에 접근하기 전에 변수에 대한 수정이 완료되도록 합니다.

사용가능한 동기화 도구에 대한 더 자세한 내용은 Synchronization Tools을 참고하세요.

Inter-thread Communication

좋은 설계는 필요한 통신량을 최소화하지만, 어느 시점에서는 스레드 간의 통신이 필요하게 됩니다.(스레드의 역할은 앱에 대한 작업 이지만, 그 작업의 결과가 전혀 사용되지 않는다면 무슨 소용이 있을까요?) 스레드는 새로운 작업(job) 요청을 처리하거나 진행 상황을 앱의 메인 스레드에 보고해야 할 수 있습니다. 이러한 상황에서는 한 스레드에서 다른 스레드로 정보를 가져오는 방법이 필요합니다. 다행히 스레드가 동일한 프로세스 공간을 공유한다는 사실은 통신 옵션이 많다는 것을 의미합니다.

스레드 간 통신 방법에는 여러 가지가 있으며, 각각의 방법마다 장단점이 있습니다. 스레드 로컬 저장소 구성(Configuring Thread-Local Storage)에는 OS X에서 사용할 수 있는 가장 일반적인 통신 메커니즘이 나열되어 있습니다.(메시지 큐 및 코코아 distributed 객체를 제외하고, 이러한 기술은 iOS에서도 사용할 수 있습니다) 이 표의 기술은 복잡성이 증가하는 순서로 나열됩니다.

메커니즘 설명
다이렉트 메시징 코코아 앱은 다른 스레드에서 셀렉터를 직접 수행하는 기능을 지원합니다. 이 기능은 한 스레드가 기본적으로 다른 스레드에서 메서드를 실행할 수 있음을 의미합니다. 타겟 스레드의 컨텍스트에서 실행되기 때문에 이 방법으로 보낸 메시지는 해당 스레드에서 자동으로 직렬화(serialized)됩니다. 입력 소스에 대한 정보는 Cocoa Perform Selector Sources를 참고하세요
전역 변수, 공유 메모리 및 객체 두 스레드 간에 정보를 전달하는 또 다른 간단한 방법은 전역 변수, 공유 객체 또는 공유 메모리 블록을 사용하는 것입니다. 공유 변수는 빠르고 간단하지만, 다이렉트 메시징보다 취약합니다. 공유 변수는 코드의 정확성을 보장하기 위해 잠금 또는 기타 동기화 메커니즘으로 주의 깊게 보호되어야 합니다. 그렇게 하지 않으면 경쟁 상태, 데이터 손상 또는 충돌이 발생할 수 있습니다.
컨디션 컨디션은 스레드가 코드의 특정 부분을 실행할 때를 제어하는데 사용할 수 있는 동기화 도구입니다. 컨디션은 명시된 조건이 충족될 때만 스레드가 실행되도록 하는 문지기로 생각할 수 있습니다. 컨디션 사용법에 대한 정보는 Using Conditions을 참고하세요.
런 루프 소스 커스텀 런 루프 소스는 스레드에서 앱 별 메시지를 수신하도록 설정한 소스입니다. 이벤트 기반이기 때문에 런 루프 소스는 할 일이 없을 때 자동으로 스레드를 절전 모드로 전환하여 스레드의 효율성을 향상시킵니다. 런 루프와 런 루프 소스에 대한 내용은 Run Loops을 참고하세요.
포트와 소켓 포트 기반 통신은 두 스레드 간의 보다 정교한 통신 방법이지만 매우 안정적인 기술이기도 합니다. 더 중요한 것은, 포트와 소켓을 사용해 다른 프로세스 및 서비스와 같은 외부 엔티티와 통신할 수 있다는 것입니다. 효율성을 위해, 포트는 런 루프 소스를 사용해 구현되므로 포트에서 대기 중인 데이터가 없을 때 스레드가 절전 모드로 전환됩니다. 런 루프 및 포트 기반 입력 소스에 대한 내용은 Run Loops를 참고하세요.
메시지 큐 레거시 멀티프로세싱 서비스는 들어오고 나가는 데이터를 관리하기 위한 FIFO(선입선출) 큐 추상화를 정의합니다. 메시지 큐는 간단하고 편리하지만 일부 다른 통신 기술만큼 효율적이지는 않습니다. 메시지 큐 사용방법에 대한 더 자세한 내용은 Multiprocessing Services Programming Guide 을 참고해주세요.
코코아 분산(distributed) 객체 분산 객체는 포트 기반 통신의 고수준 구현을 제공하는 코코아 기술입니다. 스레드 간 통신에 이 기술을 사용할 수 있지만, 이는 큰 오버헤드를 발생시키 때문에 권장하지는 않습니다. 분산 객체는 프로세스 간 이동 오버헤드가 이미 높은 다른 프로세스와 통신하는데 훨씬 더 적합하니다. 더 자세한 내용은 Distributed Objects Programming Topics을 참고하세요.

Design Tips

다음 섹션에서는 코드의 정확성을 보장하는 방식으로 스레드를 구현하는데 도움이 되는 가이드라인을 제공합니다. 가이드라인 중 일부는 자체 스레드 코드로 더 나은 성능을 달성하기 위한 팁도 제공합니다. 다른 성능 팁과 마찬가지로 코드를 변경하기 전, 변경하는 동안, 변한 후에 항상 관련 성능 통계를 수집해야 합니다.

Avoid Creating Threads Explicitly

스레드 생성 코드를 수동으로 작성하는 것은 지루하고 잠재적으로 오류가 발생하기 쉬우므로 가능한 피해야 합니다. OS X 및 iOS는 다른 API를 통해 동시성에 대한 암시적 지원을 제공합니다. 스레드를 직접 만드는 대신, 비동기 API, GCD 또는 operation 객체를 사용해 작업(work)을 수행하는 것이 좋습니다. 이러한 기술은 백그라운드에서 스레드 관련 작업을 수행하고 올바른 수행을 보장합니다. 또한 GCD 및 operation 객체와 같은 기술은 현재 시스템 로드를 기반으로 활성 스레드 수를 조정하여 직접 작성한 코드보다 훨씬 효율적으로 스레드를 관리하도록 설계되었습니다. GCD와 operation 객체에 대한 더 자세한 내용은 Concurrency Programming Guide을 참고하세요.

Keep Your Threads Reasonably Busy

스레드를 수동으로 만들고 관리하기로 결정했다면, 스레드가 귀중한 시스템 리소스를 소비한다는 점을 기억하세요. 스레드에 할당한 작업이 합리적으로 오래 지속되고 생산적인지 확인하기 위해 최선을 다해야 합니다. 동시에 대부분의 시간을 유휴 상태로 보내는 스레드를 종료하는 것을 두려워해서는 안됩니다. 스레드는 적지 않은 양의 메모리를 사용하며 일부는 시스템에 연결되어 있으므로, 유휴 스레드를 해제하면 앱의 메모리 공간을 줄이는데 도움이 될 뿐만 아니라 다른 시스템 프로세스가 사용할 수 있도록 더 많은 물리적 메모리를 확보할 수 있습니다.

중요: 유휴 스레드 종료를 시작하기 전에 항상 앱의 현재 성능에 대한 측정 기준 집합(a set of baseline measurements)을 기록해야 합니다. 변경을 시도한 후 추가 측정을 수행하여 변경이 성능을 저하시키는 것이 아니라 실제로 성능을 개선하는지 확인하세요.

Avoid Shared Data Structures

스레드 관련 리소스 충돌을 피하는 가장 간단하고 쉬운 방법은 프로그램의 각 스레드에 필요한 데이터의 고유한 복사본을 제공하는 것 입니다. 병렬 코드는 스레드 간의 통신 및 리소스 경쟁을 최소화할 때 가장 잘 작동합니댜.

다중 스레드 앱을 만드는 것은 어렵습니다. 코드의 모든 적절한 지점에서 매우 주의하고, 공유 데이터 구조를 잠그더라도 코드는 여전히 의미론적으로 안전하지 않을 수 있습니다. 예를 들어, 공유 데이터 구조가 특정 순서로 수정될 것으로 예상되는 경우, 코드에 문제가 발생할 수 있습니다. 문제 발생을 막기 위해 코드를 트랜잭션 기반 모델로 변경하면 결과적으로 다중 스레드에서의 성능 이점이 무효화될 수 있습니다. 리소스 경쟁을 처음부터 제거하면 종종 뛰어난 성능과 함께 더 단순한 디자인이 됩니다.

Be Aware of Thread Behaviors at Quit Time

분리되지 않은(non-detached) 모든 스레드가 종료될 때까지 프로세스가 실행됩니다. 기본적으로, 앱의 메인스레드만 분리되지 않은 상태로 생성되지만, 다른 스레드도 분리되지 않은 상태로 생성할 수 있습니다. 사용자가 앱을 종료할 때 분리된 스레드가 수행하는 작업(work)은 선택 사항으로 간주되므로, 일반적으로 분리된 모든 스레드를 즉시 종료하는 하는 것이 적절합니다. 그러나 앱이 백그라운드 스레드를 사용해 데이터를 디스크에 저장하거나 다른 중요한 작업(work)을 수행하는 경우, 앱이 종료될 때 데이터 손실을 방지하기 위해 이러한 스레드를 분리되지 않은 스레드로 생성할 수 있습니다.

스레드를 분리되지 않은(joinable이라고도 함) 상태로 생성하려면 추가적인 작업이 필요합니다. 대부분의 고수준의 스레드 기술들은 기본적으로 joinable 스레드를 생성하지 않기 때문에, POSIX API를 사용해 스레드를 생성해야 할 수도 있습니다. 또한, 분리되지 않은 스레드가 마지막으로 종료될 때 결합하려면 앱의 메인 스레드에 코드를 추가해야 합니다. joinable 스레드 생성에 대한 정보는 Setting the Detached State of a Thread를 확인하세요.

코코아 앱을 구현하고 있다면,applicationShouldTerminate: 델리게이트 메서드를 사용해 앱 종료를 좀 더 지연시키거나 완전히 취소할 수도 있습니다. 종료를 지연시킬 때, 앱은 중요한 스레드가 작업을 완료할 때까지 기다린 다음 replyToApplicationShouldTerminate: 메서드를 호출해야 합니다. 이 메서드들에 대한 더 자세한 정보는 NSApplication Class Reference를 참고하세요.

Handle Exceptions

예외 처리 메커니즘은 예외가 throw될 때 필요한 정리를 수행하기 위해 현재 콜스택에 의존합니다. 각 스레드에는 고유한 콜스택이 존재하므로, 각 스레드는 고유한 예외를 캐치해야 합니다. 보조 스레드에서 예외를 캐치하지 못하는 것은 메인 스레드에서 예외를 캐치하지 못하는 것과 같습니다: 스레드를 소유하고 있는 스레드가 종료됩니다. 프로세싱을 위해 다른 스레드에 캐치되지 않은 예외를 throw할 수 없습니다.

현재 스레드의 예외적인 상황을 다른 스레드(예를 들어 메인 스레드)에 알려야 하는 경우, 예외를 캐치하고 발생한 상황을 나타내는 메시지를 다른 스레드에 전송하면 됩니다. 모델과 시도하려는 것에 따라 예외를 캐치한 스레드는 프로세싱을 계속하거나(가능하다면), 지시를 기다리거나 단순히 종료할 수 있습니다.

Note: 코코아에서, NSException 객체는 일단 캐치되면, 스레드에서 스레드로 전달할 수 있는 self-contained 객체입니다.

경우에 따라 예외 핸들러가 자동으로 생성될 수 있습니다. 예를 들어 Objective-C의 @synchronized 구문에는 암시적 예외 핸들러가 포함되어 있습니다.

Terminate Your Threads Cleanly

스레드가 종료되는 가장 좋은 방법은 자연스럽게 메인 진입점 루틴의 끝에 도달하도록 하는 것입니다. 스레드를 즉시 종료하는 함수가 있지만, 이 함수는 최후의 수단으로만 사용해야 합니다. 스레드가 자연스러운 엔드포인트에 도달하기 전에 스레드를 종료하면 스레드가 자체적으로 정리되는 것을 방지할 수 있습니다. 스레드가 메모리를 할당하거나, 파일을 열거나, 다른 유형의 리소스를 획득한 경우, 코드가 해당 리소스를 회수하지 못해 메모리 누수 또는 기타 잠재적인 문제가 발생할 수 있습니다.

스레드를 종료하는 적절한 방법에 대한 자세한 내용은 Terminating a Thread를 참고하세요.

Thread Safety in Libraries

앱 개발자는 앱이 여러 스레드로 실행되는지 여부를 제어할 수 있지만 라이브러리 개발자는 그렇지 않습니다. 라이브러리를 개발할 때, 호출하는 앱이 다중 스레드이거나 언제든지 다중 스레드로 전환할 수 있다고 가정해야 합니다. 결과적으로 코드의 중요한 부분에는 항상 잠금을 사용해야 합니다.

라이브러리 개발자의 경우, 앱이 다중 스레드가 될 때만 잠금을 만드는 것은 현명하지 않습니다. 특정 시점에서 코드를 잠글 필요가 있는 경우, 라이브러리 사용 초기에 lock 객체를 생성하고, 가급적이면 라이브러리를 초기화하기 위한 일종의 명시적 호출에서 lock 객체를 생성하는 것이 좋습니다. 정적 라이브러리 초기화 함수를 사용해 이러한 잠금을 생성할 수 있지만, 다른 방법이 없을 때만 그렇게 하세요. 초기화 함수를 실행하면 라이브러리를 로드하는 데 필요한 시간이 추가되고 성능이 저하될 수 있습니다.

Note: 라이브러리 내에서 뮤텍스 lock을 잠그고 잠금을 해제하기 위한 호출의 균형을 항상 기억하세요. 또한 스레드로부터 안전한 환경(thread-safe environment)을 제공하기 위해 호출 코드에 의존하기 보다는 라이브러리 데이터 구조를 잠그는 것을 기억해야 합니다.

코코아 라이브러리를 개발하고 있는 경우, 앱이 다중 스레드가 될 때 알림을 받고 싶다면 NSWillBecomeMultiThreadedNotification 에 대한 옵저버를 등록할 수 있습니다. 그러나 이 알림 수신에 의존해서는 안됩니다. 라이브러리 코드가 호출되기 전에 발송될 수 있기 때문입니다.

댓글남기기