8장에서 배운 것처럼, 논리적 제어 흐름(logical control flow)이 시간상으로 겹치면 동시성(concurrent)을 갖는다고 합니다. 동시성(concurrency)이라고 알려진 이 일반적인 현상은 컴퓨터 시스템의 다양한 레벨에서 나타납니다.
지금까지 우리는 동시성을 주로 운영체제 커널이 여러 애플리케이션 프로그램을 실행하기 위해 사용하는 메커니즘으로 다루었습니다.
하지만 동시성은 커널에만 국한되지 않고 애플리케이션 프로그램에서도 중요한 역할을 합니다. (예: 리눅스 시그널 핸들러가 Ctrl+C 입력 같은 비동기 이벤트에 응답하는 것)
애플리케이션 레벨의 동시성은 다음과 같은 방식들로 유용합니다:
free 작업의 속도를 높이기 위해, 메모리 병합(coalescing) 작업을 낮은 우선순위의 동시 흐름으로 지연시켜 나중에 처리함)애플리케이션 레벨 동시성을 사용하는 프로그램을 동시성 프로그램(concurrent programs)이라고 합니다. 최신 운영체제는 이를 구축하기 위한 세 가지 기본 접근 방식을 제공합니다:
동시성 프로그램을 구축하는 가장 간단한 방법은 fork, exec, waitpid와 같이 익숙한 함수를 사용하여 프로세스를 활용하는 것입니다.
예를 들어, 동시성 서버를 구축하는 자연스러운 접근 방식은 부모 프로세스가 클라이언트 연결 요청을 accept (수락)하고, 새로운 각 클라이언트를 서비스하기 위해 새로운 자식 프로세스를 생성하는 것입니다.
이것이 어떻게 작동하는지 보기 위해, 2개의 클라이언트와 리스닝 디스크립터(예: 3번)에서 연결 요청을 기다리는 서버가 있다고 가정해 봅시다.
클라이언트 1 연결

accept)하고, 연결 디스크립터(connected descriptor, 예: 4번)를 반환받습니다. (Figure 12.1)자식 프로세스 생성 (fork)
fork를 통해 자식 프로세스를 생성합니다.디스크립터 정리 (매우 중요)

부모와 자식의 연결 디스크립터는 동일한 파일 테이블 엔트리(file table entry)를 가리킵니다.
만약 부모 프로세스가 자신의 연결 디스크립터 사본(4번)을 닫지 않는다면, (자식 프로세스가 종료된 후에도) 이 파일 테이블 엔트리는 절대 릴리즈(release)되지 않을 것입니다.
결과적으로 이는 메모리 누수(memory leak)로 이어지고, 결국 가용 메모리를 모두 소모하여 시스템을 다운시킬 수 있습니다.
클라이언트 2 연결

두 번째 자식 생성

fork를 호출하여 두 번째 자식 프로세스를 생성합니다.이 시점에서 부모는 다음 연결 요청을 기다리고, 두 자식 프로세스는 각자의 클라이언트를 동시에(concurrently) 서비스하게 됩니다.

Figure 12.5는 프로세스를 기반으로 하는 동시성 에코 서버의 코드를 보여줍니다. (29행에서 호출되는 echo 함수는 Figure 11.22의 것입니다.)
이 서버 코드에는 몇 가지 중요한 점이 있습니다:
SIGCHLD 핸들러 (좀비 회수)SIGCHLD 핸들러를 반드시 포함해야 합니다 (lines 4–9). SIGCHLD 핸들러가 실행되는 동안 SIGCHLD 시그널은 블록(blocked)되고, 리눅스 시그널은 큐(queued)에 쌓이지 않기 때문에, SIGCHLD 핸들러는 (한 번에) 여러 좀비 자식을 회수할 준비가 되어 있어야 합니다.connfd 닫기 (메모리 누수 방지)connfd (연결 디스크립터) 사본을 반드시 닫아야 합니다 (부모 line 33, 자식 line 30). 앞서 언급했듯이, 이는 특히 부모가 메모리 누수(memory leak)를 피하기 위해 매우 중요합니다.connfd 사본이 모두 닫힐 때까지 (즉, 참조 카운트가 0이 될 때까지) 실제로 종료되지 않습니다.프로세스는 부모와 자식 간에 상태 정보를 공유하는 깔끔한 모델을 가지고 있습니다: 파일 테이블은 공유되지만 사용자 주소 공간은 공유되지 않습니다.
프로세스마다 별도의 주소 공간을 갖는 것은 장점이자 단점입니다.
여러분은 이미 이 텍스트에서 여러 IPC(프로세스 간 통신) 예시를 접했습니다.
waitpid 함수와 시그널(signals)은 같은 호스트에서 실행 중인 프로세스 간에 아주 작은 메시지를 보낼 수 있게 해주는 원시적인(primitive) IPC 메커니즘입니다.하지만, "Unix IPC"라는 용어는 일반적으로 같은 호스트에서 실행 중인 다른 프로세스들과 통신할 수 있게 해주는 잡다한(hodgepodge) 기술들을 지칭할 때 예약되어 있습니다.
에코 서버가 다음 두 가지 독립적인 I/O 이벤트를 처리해야 한다고 가정해 봅시다:
어떤 이벤트를 먼저 기다려야 할까요? 어느 쪽도 이상적이지 않습니다.
accept에서 연결 요청을 기다리면 (블로킹되면), 표준 입력으로 들어온 명령에 응답할 수 없습니다.read에서 입력 명령을 기다리면 (블로킹되면), 새로운 클라이언트 연결 요청에 응답할 수 없습니다.select 함수이 딜레마의 한 가지 해결책은 I/O 멀티플렉싱(I/O multiplexing)이라는 기법입니다.
기본 아이디어는 select 함수를 사용하여 커널에 프로세스를 일시 중단(suspend)하도록 요청하고, 하나 이상의 I/O 이벤트가 발생한 후에만 애플리케이션으로 제어권을 반환받는 것입니다.
select 함수와 fd_set 매크로select 함수는 fd_set이라는 디스크립터 집합(descriptor sets)을 다룹니다.
#include <sys/select.h>
/*
* n: 감시할 파일 디스크립터 범위 (최대 디스크립터 번호 + 1)
* fdset (read set): 읽기 이벤트를 감시할 디스크립터 집합
* 리턴값: 준비된 디스크립터의 개수, 오류 시 -1
*/
int select(int n, fd_set *fdset, NULL, NULL, NULL);
/* fd_set 조작 매크로 */
FD_ZERO(fd_set *fdset); /* fdset의 모든 비트를 0으로 초기화 */
FD_SET(int fd, fd_set *fdset); /* fdset에 fd 비트를 1로 설정 (추가) */
FD_CLR(int fd, fd_set *fdset); /* fdset에서 fd 비트를 0으로 설정 (제거) */
FD_ISSET(int fd, fd_set *fdset); /* fdset에 fd 비트가 1인지 확인 */
논리적으로, fd_set은 비트 벡터(bit vector)입니다. k번째 비트 b_k가 1이면 디스크립터 k가 집합에 포함되었음을 의미합니다.
select 함수의 동작 방식select 함수는 fdset (우리가 읽기 집합(read set)이라 부름)을 입력으로 받습니다.select는 read_set에 포함된 디스크립터 중 최소 하나라도 "읽기 준비"가 될 때까지 프로세스를 블록시킵니다.select가 리턴될 때, 커널은 입력으로 전달한 fdset을 수정하여, 읽기 준비가 된 디스크립터들로만 구성된 부분집합인 준비된 집합(ready set)으로 덮어씁니다.ready_set에 포함된 디스크립터의 개수(cardinality)를 반환합니다.⚠️ 중요: 이 부작용 때문에, select가 리턴된 후 루프를 돌아 다시 select를 호출하기 전에는 매번 read_set을 원본으로 다시 갱신해줘야 합니다.
select를 사용한 반복 서버 (Figure 12.6)
select를 사용하여 표준 입력과 리스닝 디스크립터를 동시에 처리하는 서버 예제입니다.
read_set)listenfd를 열고(line 16), FD_ZERO를 호출하여 빈 read_set을 만듭니다.read_set (Ø): [0 0 0 0] (stdin=0, listenfd=3 가정)read_set 구성 (감시 대상 추가)FD_SET을 사용하여 STDIN_FILENO (0번)과 listenfd (3번)을 read_set에 추가합니다.read_set ({0,3}): [1 0 0 1]select 호출 (대기)select를 호출합니다(line 24). select는 listenfd나 stdin 둘 중 하나라도 읽기 가능해질 때까지 블록됩니다.stdin(0번) 디스크립터가 "읽기 준비" 상태가 됩니다.select가 리턴되며, fdset 인자를 ready_set으로 덮어씁니다.ready_set ({0}): [0 0 0 1]ready_set 확인 (이벤트 처리)select가 리턴된 후, FD_ISSET 매크로를 사용하여 어떤 디스크립터가 준비되었는지 확인합니다.FD_ISSET(STDIN_FILENO, ...) (line 25): 표준 입력이 준비되었으므로 command() 함수를 호출하여 명령을 처리합니다.FD_ISSET(listenfd, ...) (line 27): 리스닝 디스크립터가 준비되었다면 accept를 호출하고 echo 함수를 실행합니다.이 프로그램은 select의 좋은 예시이지만, 여전히 아쉬운 점이 있습니다.
echo 함수(line 30)는 클라이언트가 연결을 끊을 때까지(EOF) 계속해서 입력을 에코합니다.echo를 수행하는 동안(블로킹됨), 관리자가 표준 입력에 명령을 입력해도 서버는 해당 클라이언트 작업이 끝날 때까지 명령에 응답하지 않습니다.I/O 멀티플렉싱은 이벤트 기반 동시성 프로그램(concurrent event-driven programs)의 기초로 사용될 수 있습니다. 이벤트 기반 프로그램에서는 특정 이벤트의 결과로 논리적 흐름이 진행됩니다.
I/O 멀티플렉싱 기반의 동시성 서버는 새로운 클라이언트 마다, 새로운 상태 머신 를 생성하고 이를 연결 디스크립터 와 연결합니다. (Figure 12.7에서 설명하듯이)

서버는 select 함수를 사용하여 이러한 입력 이벤트의 발생을 ㄹ감지합니다. 각 연결 디스크립터가 읽기 준비가 되면, 서버는 해당 상태 머신에 대한 트랜지션(즉, 디스크립터에서 텍스트 한 줄을 읽고 에코하는 작업)을 실행합니다.
Figure 12.8은 I/O 멀티플렉싱 기반의 동시성 이벤트 기반 서버의 전체 예제 코드입니다.
main 함수 (Figure 12.8)
서버는 활성 클라이언트 집합을 pool 구조체(lines 3–11)로 관리합니다.
init_pool(line 27)을 호출하여 풀을 초기화합니다.select 함수를 호출하여(line 32) 두 가지 종류의 입력 이벤트를 감지합니다.listenfd가 준비됨)connfd가 준비됨)listenfd가 준비되면 (즉, 새 연결 요청이 도착하면, line 35), 서버는 연결을 수락(Accept, line 37)하고 add_client 함수를 호출하여 클라이언트를 풀에 추가합니다(line 38).check_clients 함수를 호출하여(line 42) 준비된 각 연결 디스크립터로부터 텍스트 한 줄씩 에코합니다. (이전 12.2 예제의 문제를 해결함)init_pool 함수 (Figure 12.9)
클라이언트 풀을 초기화합니다.
clientfd 배열은 연결 디스크립터 집합을 나타내며, 1은 사용 가능한 슬롯을 의미합니다.1로 설정합니다(lines 5–7).listenfd가 select의 read_set에 포함된 유일한 디스크립터입니다(lines 10–12). (즉, 처음엔 새 연결만 감시)add_client 함수 (Figure 12.10)
새로운 클라이언트를 활성 클라이언트 풀에 추가합니다.
clientfd 배열에서 빈 슬롯(-1인 곳)을 찾습니다(line 5).connfd를 배열에 추가하고(line 8), rio_readlineb를 호출할 수 있도록 해당 클라이언트를 위한 Rio 읽기 버퍼를 초기화합니다(line 9).FD_SET: 새 connfd를 select의 read_set에 추가합니다(line 12). (★ 이제부터 이 클라이언트의 데이터 도착도 감시 대상이 됩니다.)maxfd (lines 15–16): select 함수에 넘겨줄 디스크립터 번호의 최댓값을 추적합니다.maxi (lines 17–18): check_clients 함수가 전체 배열을 검색할 필요 없이, 현재까지 사용된 clientfd 배열의 가장 큰 인덱스를 추적합니다. (최적화)check_clients 함수 (Figure 12.11)
준비된(ready) 각 연결 디스크립터로부터 텍스트 한 줄을 에코합니다.
maxi 인덱스까지만 루프를 돕니다(line 7).FD_ISSET: select가 반환한 ready_set을 확인하여, 현재 디스크립터(connfd)가 "읽기 준비" 상태인지 확인합니다(line 12).rio_readlineb를 호출하여 텍스트 한 줄을 읽습니다(line 15).rio_readlineb가 0을 반환)Close(connfd)로 연결을 닫습니다(line 23).FD_CLR: read_set에서 이 디스크립터를 제거합니다(line 24). (더 이상 감시하지 않음)clientfd[i] = -1: 풀(pool)에서 해당 슬롯을 비워 재사용할 수 있게 합니다(line 25).select 함수: 입력 이벤트를 감지합니다.add_client 함수: 새로운 논리적 흐름 (상태 머신)을 생성합니다.check_clients 함수: 입력 라인을 에코함으로써 상태 전이(transition)를 수행하고, 클라이언트가 완료되면(EOF) 상태 머신을 삭제합니다.Figure 12.8의 서버(이벤트 기반 서버)는 I/O 멀티플렉싱 기반 이벤트 기반 프로그래밍의 장단점을 잘 보여줍니다.
gdb와 같은 익숙한 디버깅 도구를 사용하여 일반적인 순차 프로그램처럼 동시성 서버를 디버깅할 수 있다는 것입니다.지금까지 우리는 동시성 논리 흐름을 만들기 위한 두 가지 접근 방식을 살펴보았습니다.
이 섹션에서는 이 두 가지 방식의 하이브리드(hybrid)인 세 번째 접근 방식, 즉 스레드(threads)를 소개합니다.
스레드 기반 논리 흐름은 프로세스 기반 흐름과 I/O 멀티플렉싱 기반 흐름의 특징을 결합합니다.
여러 스레드의 실행 모델은 여러 프로세스의 실행 모델과 어떤 면에서는 유사합니다. (Figure 12.12 참고)

read나 sleep 같은 느린 시스템 콜을 실행하거나, 또는 시스템의 인터벌 타이머에 의해 인터럽트가 걸려서) 컨텍스트 스위치를 통해 피어 스레드로 제어권이 넘어갑니다. 피어 스레드가 잠시 실행되다가 다시 메인 스레드로 제어권이 넘어오는 식으로 반복됩니다.스레드 실행은 몇 가지 중요한 면에서 프로세스와 다릅니다.
Posix 스레드(Pthreads)는 C 프로그램에서 스레드를 조작하기 위한 표준 인터페이스입니다. 1995년에 채택되었으며 모든 리눅스 시스템에서 사용할 수 있습니다. Pthreads는 약 60개의 함수를 정의하며, 프로그램이 스레드를 생성(create), 종료(kill), 회수(reap)하고, 피어 스레드와 데이터를 안전하게 공유하며, 시스템 상태의 변경을 피어 스레드에게 알릴 수(notify) 있게 해줍니다.

Figure 12.13은 간단한 Pthreads 프로그램입니다.
Hello, world!\n를 출력하고 종료합니다.exit를 호출하여 프로세스를 종료시킵니다.이것은 우리가 처음 본 스레드 프로그램이므로, 자세히 분석해 보겠습니다.
스레드의 코드와 지역 데이터는 스레드 루틴(thread routine) (이 예제에서는 thread 함수) 내에 캡슐화됩니다.
void *vargp)를 입력으로 받고, 제네릭 포인터(void *)를 반환합니다.struct에 넣고 그 struct의 포인터를 전달해야 합니다. (여러 값을 반환할 때도 마찬가지)(Lines 12-16) 피어 스레드를 위한 스레드 루틴입니다.
return 문을 실행하여 피어 스레드 자신을 종료시킵니다.Line 4는 메인 스레드의 코드 시작점입니다.
pthread_t tid): 피어 스레드의 스레드 ID(TID)를 저장하기 위한 tid 변수를 선언합니다.pthread_create(...)):pthread_create 호출이 리턴되면, 메인 스레드와 새로 생성된 피어 스레드는 동시적으로 실행됩니다.tid 변수에는 새로 생성된 스레드의 ID가 저장됩니다.pthread_join(tid, NULL)):tid가 식별하는 피어 스레드가 종료될 때까지 기다립니다(block).exit(0)):pthread_join이 리턴된 후 (즉, 피어 스레드가 종료된 후), 메인 스레드는 exit 함수를 호출합니다.exit 함수는 프로세스 내에서 현재 실행 중인 모든 스레드를 종료시킵니다. (이 경우에는 피어 스레드는 이미 종료되었으므로, 메인 스레드 자신만 종료됩니다.)스레드는 pthread_create 함수를 호출하여 다른 스레드를 생성합니다.
#include <pthread.h>/* 스레드 루틴(함수)의 타입 정의 (void*를 받아 void*를 리턴) */
typedef void *(func)(void *);
int pthread_create(pthread_t *tid, pthread_attr_t *attr,
func *f, void *arg);
// 성공 시 0, 오류 시 0이 아닌 값 반환
pthread_create 함수는 새로운 스레드를 생성하고, 이 새 스레드의 컨텍스트에서 스레드 루틴(함수) f를 실행시키며, 이때 arg를 입력 인자로 전달합니다.
tid: (출력 인자) pthread_create가 리턴될 때, 새로 생성된 스레드의 ID가 이 포인터가 가리키는 곳에 저장됩니다.attr: 새 스레드의 기본 속성을 변경하는 데 사용될 수 있습니다. (이 속성 변경은 이 책의 범위를 벗어나며, 예제에서는 항상 NULL을 인자로 전달할 것입니다.)f: 새 스레드가 실행할 스레드 루틴 (함수 포인터).arg: f 함수에 전달될 단일 입력 인자.새로 생성된 스레드는 pthread_self 함수를 호출하여 자기 자신의 스레드 ID를 확인할 수 있습니다.
#include <pthread.h>
pthread_t pthread_self(void);
// 호출한 스레드의 ID를 반환
스레드는 다음 방식 중 하나로 종료됩니다:
pthread_exit 함수를 호출하여 스레드가 명시적으로(explicitly) 종료됩니다.주의: 만약 메인 스레드가 pthread_exit를 호출하면, 메인 스레드는 다른 모든 피어 스레드들이 종료될 때까지 기다립니다. 그 후, 메인 스레드와 전체 프로세스를 thread_return 값과 함께 종료시킵니다.
#include <pthread.h>
void pthread_exit(void *thread_return);
// (절대 반환하지 않음)
exit 함수를 호출합니다. 이 함수는 프로세스와 그 프로세스에 연관된 모든 스레드를 즉시 종료시킵니다.pthread_cancel 함수를 호출함으로써, 현재 스레드를 (강제로) 종료시킵니다.#include <pthread.h>
int pthread_cancel(pthread_t tid);
// 성공 시 0, 오류 시 0이 아닌 값 반환스레드는 pthread_join 함수를 호출하여 다른 스레드가 종료되기를 기다립니다.
#include <pthread.h>
int pthread_join(pthread_t tid, void **thread_return);
// 성공 시 0, 오류 시 0이 아닌 값 반환
pthread_join 함수는 다음 작업들을 수행합니다.
tid가 종료될 때까지 블록(block)됩니다 (기다립니다).void *) 포인터 값을 thread_return이 가리키는 위치에 할당합니다 (즉, 반환 값을 받아옵니다).pthread_join의 한계리눅스의 wait 함수(프로세스용)와 달리, pthread_join 함수는 오직 특정 스레드(tid)가 종료되는 것만 기다릴 수 있다는 점에 유의해야 합니다.
pthread_join에게 (프로세스의 wait(-1, ...)처럼) "임의의(arbitrary) 스레드 중 아무나 하나가" 종료되기를 기다리라고 지시할 방법이 없습니다.
이러한 제약은 우리가 덜 직관적인 다른 메커니즘을 사용하도록 강요함으로써 코드를 복잡하게 만들 수 있습니다. (실제로 Stevens는 이것이 Posix 명세(specification)의 버그라고 강력하게 주장합니다 [110].)
어느 시점에서든 스레드는 결합 가능(joinable) 상태이거나 분리된(detached) 상태입니다.
pthread_join)되거나 종료(killed)될 수 있습니다.기본적으로 스레드는 결합 가능(joinable) 상태로 생성됩니다. 메모리 누수(memory leaks)를 피하기 위해, 모든 joinable 스레드는 (1) 다른 스레드에 의해 명시적으로 회수되거나 (pthread_join), (2) pthread_detach 함수 호출을 통해 분리되어야 합니다.
#include <pthread.h>
int pthread_detach(pthread_t tid);
// 성공 시 0, 오류 시 0이 아닌 값 반환
pthread_detach 함수는 joinable 스레드인 tid를 분리시킵니다.
스레드는 pthread_detach(pthread_self())를 인자로 호출하여 자기 자신을 분리시킬 수 있습니다.
(이 책의) 일부 예제에서는 joinable 스레드를 사용하지만, 실제 프로그램에서는 detached 스레드를 사용할 충분한 이유가 있습니다.
join) 것은 불필요하며 바람직하지도 않습니다.pthread_once 함수는 스레드 루틴과 연관된 (공유) 상태를 (단 한 번만) 초기화할 수 있게 해줍니다.
#include <pthread.h>// 제어 변수 초기화
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
// (항상 0을 반환)
once_control 변수는 전역(global) 변수 또는 정적(static) 변수여야 하며, 항상 PTHREAD_ONCE_INIT 값으로 초기화해야 합니다.
동작 방식:once_control 변수를 인자로 하여 pthread_once를 처음 호출하면, 이 함수는 init_routine을 호출합니다. (init_routine은 입력 인자와 반환 값이 없는 함수입니다.)
이후 동일한 once_control 변수를 사용하여 pthread_once를 다시 호출하면 (다른 스레드에서 호출하더라도) 아무 작업도 수행하지 않습니다.
용도:pthread_once 함수는 여러 스레드에 의해 공유되는 전역 변수를 동적으로 (그리고 딱 한 번만) 초기화해야 할 때 유용합니다. (12.5.5절에서 예제를 살펴볼 것입니다.)
Figure 12.14는 스레드를 기반으로 하는 동시성 에코 서버의 코드입니다.

전체적인 구조는 프로세스 기반 설계와 유사합니다. 메인 스레드가 반복적으로 연결 요청을 기다린 다음, 그 요청을 처리할 피어 스레드를 생성합니다.
코드는 간단해 보이지만, 우리가 자세히 살펴봐야 할 일반적이면서도 미묘한 두 가지 이슈가 있습니다.
connfd 전달 시 발생하는 경쟁 상태 (Race Condition)첫 번째 이슈는 pthread_create를 호출할 때 연결 디스크립터(connfd)를 피어 스레드에 어떻게 전달하는가입니다. 가장 뻔한 접근 방식은 디스크립터의 주소를 전달하는 것입니다.
[잘못된 예시]
// (메인 스레드)
connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen);
Pthread_create(&tid, NULL, thread, connfd); // connfd의 주소를 전달
// (피어 스레드)
void *thread(void *vargp) {
int connfd = *((int *)vargp); // 포인터를 역참조
...
}
하지만 이 방식은 잘못되었습니다. 이는 피어 스레드의 할당문(connfd = ...)과 메인 스레드의 accept문 사이에 경쟁 상태(race)를 유발하기 때문입니다.
Accept로 connfd에 (예) 5를 받음.Pthread_create로 스레드 A 생성 (&connfd 전달).Accept에서 블록됨.connfd = *((int *)vargp); 실행. 5를 가져옴. (→ 성공)Accept로 connfd에 (예) 5를 받음.Pthread_create로 스레드 A 생성 (&connfd 전달).Accept 실행, connfd에 (예) 6을 받음. (메인 스레드의 connfd 값이 덮어써짐)connfd = *((int *)vargp); 실행. &connfd를 역참조하여 6을 가져옴. (→ 실패)accept가 반환하는 각 connfd를 malloc을 통해 자신만의 동적 할당 메모리 블록에 할당해야 합니다. (Figure 12.14의 21-22행 참고)다른 이슈는 스레드 루틴에서 메모리 누수를 피하는 것입니다.
pthread_join)하지 않습니다. 따라서 각 스레드는 스스로를 분리(detach)해야 합니다(line 31, pthread_detach). 이렇게 해야 스레드 종료 시 스레드의 메모리 리소스(스택 등)가 시스템에 의해 자동으로 회수됩니다.malloc으로 할당된 메모리 누수:free 해줘야 합니다(line 32).프로그래머의 관점에서 스레드의 매력적인 점 중 하나는 여러 스레드가 동일한 프로그램 변수를 쉽게 공유할 수 있다는 것입니다. 하지만 이러한 공유는 까다로울 수 있습니다. 올바르게 스레드화된(threaded) 프로그램을 작성하려면, '공유'가 무엇을 의미하고 어떻게 작동하는지 명확히 이해해야 합니다.
프로그램의 변수가 공유되는지 아닌지 이해하기 위해 다음의 기본 질문들을 짚어봐야 합니다.
어떤 변수는 여러 스레드가 해당 변수의 동일 인스턴스를 참조할 경우에만(if and only if) '공유'됩니다.

공유에 대한 논의를 구체적으로 유지하기 위해, Figure 12.15의 프로그램을 실행 예제로 사용할 것입니다. 다소 인위적이긴 하지만, 공유에 대한 여러 미묘한 점들을 설명하기 때문에 연구할 가치가 있습니다.
이 예제 프로그램은 두 개의 피어 스레드를 생성하는 메인 스레드로 구성됩니다. 메인 스레드는 각 피어 스레드에 고유 ID(0 또는 1)를 전달하고, 피어 스레드는 이 ID를 사용하여 개인화된 메시지와 함께, 스레드 루틴이 호출된 총횟수(cnt)를 출력합니다.
동시성 스레드 풀은 하나의 프로세스 컨텍스트 내에서 실행됩니다.
운영 관점에서, 한 스레드가 다른 스레드의 레지스터 값을 읽거나 쓰는 것은 불가능합니다.
반면에, 어떤 스레드든 공유 가상 메모리의 어떤 위치든 접근할 수 있습니다. 만약 한 스레드가 메모리 위치를 수정하면, 다른 모든 스레드도 (결국) 그 위치를 읽을 때 변경 사항을 보게 됩니다.
요약: 레지스터는 절대 공유되지 않으며, 가상 메모리는 항상 공유됩니다.
별도의 스레드 스택에 대한 메모리 모델은 그렇게 깔끔하지 않습니다.
이 스택들은 (공유된) 가상 주소 공간의 스택 영역에 포함되어 있으며, "보통은" 각 스레드가 자신의 스택에만 독립적으로 접근합니다.
우리가 "항상"이 아닌 "보통"이라고 말하는 이유는, 스레드 스택들이 다른 스레드로부터 보호되지 않기(not protected) 때문입니다. 따라서, 만약 한 스레드가 어떻게든 다른 스레드의 스택을 가리키는 포인터를 얻게 된다면, 그 스택의 어떤 부분이든 읽고 쓸 수 있습니다.
(Figure 12.15 예제 프로그램의 26행에서 피어 스레드가 전역 변수 ptr을 통해 메인 스레드의 스택 내용을 간접적으로 참조하는 것이 바로 이 예시입니다.)
스레드 C 프로그램의 변수들은 스토리지 클래스(storage classes)에 따라 가상 메모리에 매핑됩니다.
ptr은 런타임 시 읽기/쓰기 영역에 단 하나의 인스턴스(ptr)만 존재합니다.static 속성 없이 선언된 변수.tid (main 함수 9행): 메인 스레드의 스택에 tid.m이라는 인스턴스 하나가 존재합니다.myid (thread 함수 24행): 피어 스레드 0의 스택에 myid.p0, 피어 스레드 1의 스택에 myid.p1이라는 두 개의 개별 인스턴스가 존재합니다.static 속성을 가지고 선언된 변수.cnt (thread 함수 25행): 예제 프로그램의 각 피어 스레드가 cnt를 선언함에도 불구하고, 런타임 시에는 읽기/쓰기 영역에 단 하나의 cnt 인스턴스만 존재합니다.우리는 변수 v의 인스턴스 중 하나가 둘 이상의 스레드에 의해 참조될 경우에만 그 변수를 공유(shared)된다고 말합니다.
cnt (공유됨 O):cnt 변수(지역 정적 변수)는 공유됩니다. cnt는 런타임 시 단 하나의 인스턴스만 가지며, 이 인스턴스를 두 피어 스레드가 모두 참조하기 때문입니다.myid (공유 안 됨 X):myid 변수(지역 자동 변수)는 공유되지 않습니다. myid는 두 개의 인스턴스(각 스레드의 스택에 하나씩)를 가지며, 각각의 인스턴스는 정확히 하나의 스레드에 의해서만 참조되기 때문입니다.msgs (공유될 수 있음!):main 함수의 스택에 있는) msgs와 같은 지역 자동 변수(local automatic variable) 또한 공유될 수 있다는 것을 인지하는 것이 중요합니다. (이 경우, ptr이라는 전역 변수가 msgs를 가리키고, 피어 스레드들이 ptr을 통해 msgs에 접근하므로 공유됩니다.)