728x90
12. 동시성 프로그램
- 논리적 제어흐름은 이들이 시간적으로 중첩되면 동시적이다. 이와 같은 현상을 동시성이라고 한다.
- 응용수준 동시성은 다양한 경우 유용하다.
- 느린 I/O 디바이스 접근하기 : 응용프로그램은 유용한 작업을 I/O요청과 겹치게 한다.
- 사람들과 상호 작용하기 : 사용자가 어떤 동작을 요청할 때 마다, 이 동작을 수행하기 위해 별도의 동시성의 논리 흐름이 생성된다.
- 작업을 지연시켜서 시간지연 줄이기 : 다른 동작을 지연시키고 이들을 동시에 수행해서 특정 동작 시간 지연을 축소하기 위해 동시성을 이용한다.
- 다수의 네트워크 클라이언트 처리 : 클라이언트마다 별도의 논리흐름을 생성하는 동시성 서버
- 멀티코어 머신에서 병렬로 계산하기
- 동시성 프로그램을 만들기 위한 세 개의 기본 접근방법
- 프로세스 : 각 논리적 흐름은 커널이 스케줄하고 관리하는 프로세스이다. 프로세스가 별도의 가상 주소공간을 가지기 때문에, 서로 통신하기를 원하는 흐름들은 모종의 명시적 프로세스간 통신(IPC : interprocess communication) 메커니즘을 사용해야 한다.
- I/O 다중화 : 동시성 프로그램의 한 형태로, 응용들은 명시적으로 자신의 논리흐름을 한 개의 프로세스 컨텍스트 내에서 스케줄한다. 논리적 흐름들은 파일 식별자에 도착하는 데이터로 인해 메인 프로그램이 명시적으로 하나의 상태에서 다른 상태로 전환하는 상태머신으로 모델 할 수 있다. 프로그램이 한 개의 프로세스이므로 모든 흐름들은 동일한 주소공간을 공유한다.
- 쓰레드 : 쓰레드는 한 개의 프로세스 컨텍스트에서 돌아가는 논리흐름으로 커널에 의해 스케줄된다. 쓰레드를 다른 두 개의 방식의 하이브리드 형태로 생각할 수 있다. 커널에 의해 스케줄되며 동일한 가상 주소공간을 공유한다.
12.1 프로세스를 사용한 동시성 프로그래밍
- 동시성 프로그램을 만드는 가장 간단한 방법
- ex)동시성 서버를 구현하는 방법은 부모에서 클라이언트 연결 요청을 수락하는 것이며, 그 후에 새로운 자식 프로세스를 생성해서 새로운 각각의 클라이언트를 서비스한다.
12.1.1 프로세스 기반 동시성 서버
- 서버를 만드는 데 중요한 몇 가지 사항이 있다.
- 첫째, 서버들은 대개 장시간 동안 돌아가므로 좀비 자식을 청소하는 SIGCHID 핸들러를 포함 해야 한다.
- 둘째, 부모와 자식은 자신의 connfd사본을 닫아야 한다.
- 마지막으로, 소켓의 파일 테이블 엔트리 내의 참조 횟수 때문에 클라이언트로의 연결은 부모와 자식의 connfd 사본이 모두 닫힐 때 까지 종료되지 않는다
12.1.2 프로세스의 장단점
- 프로세스는 부모와 자식 사이에 상태 정보를 공유하는 것에 대해서 깔끔한 모델을 가지고 있다.
- 파일은 공유되고 사용자 주소공간은 공유되지 않는다.
- 프로세스들이 분리된 주소공간을 가지는 것은 장점이자 단점이다. 한 개의 프로세스가 우연히 다른 프로세스의 가상메모리에 쓰는 것은 불가능하며, 이로 인해서 많은 혼란스러운 오류들을 제거 할 수 있다.
- 반면에 별도의 주소공간은 프로세스가 상태정보를 공유하는 것을 어렵게 한다. 정보를 공유하기 위해서 명시적인 IPC메커니즘을 사용해야 한다. 그리고 프로세스 제어와 IPC의 오버헤드가 크기 때문에 더 느려지는 경향이 있다.
12.2 I/O 다중화를 이용한 동시성 프로그래밍
- 기본 아이디어는 select 함수를 사용해서 커널에게 이 프로세스를 정지할 것을 요구해서 한 개 이상의 I/O 이벤트가 발생한 후에만 응용에게 제어를 돌려준다.
12.2.1 I/O 다중화의 장단점
- 한 가지 장점은 이벤트기반 설계로 프로그래머가 프로세스 기반 설계보다 자신의 프로그램을 더 잘 제어할 수 있다는 점이다.
- 또 다른 장점은 I/O 다중화에 기초한 이벤트 기반 서버는 단일 프로세스의 컨텍스트에서 돌아가며, 그래서 모든 논리흐름은 프로세스의 전체 주소공간에 접근할 수 있다. 이 흐름들 간에 데이터 공유를 쉽게 해준다. GDB같은 친숙한 디버깅 도구를 이용해서 순차 프로그램에서 하는 것처럼 동시성 서버를 디버그 할 수 있다.
- 마지막으로 이벤트 기반 설계는 대개 프로세스 기반 설계보다 훨씬 더 효율적이며, 그 이유는 이들이 새로운 흐름을 스케줄 하기 위해서 프로세스 문맥전환을 요구하지 않기 때문이다.
- 이벤트 기반 설계의 한 가지 중요한 단점은 코딩 복잡도다. 프로세스 기반 서버보다 세 배 더 많은 코드를 필요로 한다. 불행히도 동시성의 크기가 감소하면 할수록 복잡성은 증가한다. 크기라는 것은 각 논리흐름이 타임 슬라이스 동안에 각 논리흐름이 실행하는 인스트럭션의 수를 의미한다.
- 또 다른 중요한 단점은 이들이 멀티코어 프로세서를 완전히 활용할 수 없다는 것이다.
12.3 쓰레드를 이용한 동시성 프로그래밍
- 쓰레드는 프로세스의 컨텍스트 내에서 돌아가는 논리흐름이다.
- 쓰레드는 커널에 의해서 자동으로 스케줄된다. 각 쓰레드는 고유의 정수 쓰레드ID(TID), 스택, 스택 포인터, 프로그램 카운터, 범용 레지스터, 조건 코드를 포함하는 자신만의 쓰레드 컨텍스트를 가진다. 한 개의 프로세스에서 돌고 있는 모든 쓰레드는 이 프로세스의 전체 가상주소를 공유한다.
12.3.1 쓰레드 실행모델
- 각 쓰레드는 메인 쓰레드라고 부르는 한 개의 쓰레드로 생명을 시작한다. 어던 시점에서 메인 스레드는 피어(Peer) 쓰레드를 생성하고, 이때부터 두 쓰레드가 동시에 돌아간다. 피어 쓰레드는 제어를 메인 쓰레드로 돌려주기 전에 잠시동안 실행하는 식으로 진행된다.
- 쓰레드의 실행은 일부 중요한 부분에서 프로세스와 다르다. 쓰레드 컨텍스트가 프로세스 컨텍스트보다 훨씬 작기 때문에 쓰레드 문맥전환은 프로세스보다 빠르다. 또 프로세스와는 달리 쓰레드는 경직된 부모 - 자식 계층구조에서 구성되지 않았다. 하나의 프로세스에 연계된 쓰레드들은 피어들의 풀을 구성하고, 쓰레드는 이들과는 독립적으로 다른 쓰레드에 의해서 생성되었다. 메인 쓰레드는 항상 프로세스에서 돌아가는 첫 번째 쓰레드라는 의미에서만 다른 쓰레드와 구별된 피어의 풀에 관한 이 개념의 주요 영향은 쓰레드가 자신의 피어모드를 죽일 수 있거나 자신의 피어들이 종료하는 것을 기다릴 수 있다는 것이다. 또, 각 피어는 동일한 공유 데이터를 읽고 쓸 수 있다.
12.3.2 POSIX 쓰레드,
- POSIX 쓰레드는 C프로그램에서 쓰레드를 조작하는 표준 인터페이스다. pthreads는 데이터를 피어 쓰레드와 안전하게 공유하기 위해서, 시스템 상태의 변화를 피어들에게 알리기 위해 프로그램이 쓰레드를 생성하고, 죽이고, 청소하도록하는 약 60개의 함수를 정의한다.
12.3.6 쓰레드 분리하기
- 언제나 쓰레드는 연결가능하거나 분리되어 있다. 연결 가능한 쓰레드는 다른 쓰레드에 의해 청소되고 종료될 수 있다. 자신의 메모리 자원들은 다른 쓰레드에 의해 청소될 때 까지는 반환되지 않는다. 반대로, 분리된 쓰레드는 다른 쓰레드에 의해서 청소되거나 종료 될 수 없다. 자신의 메모리 자원들은 이 쓰레드가 종료할 때 시스템에 의해 자동으로 반환된다.
- 기본적으로 쓰레드는 연결 가능하게 생성된다.
12.4 쓰레드 프로그램에서 공유변수
- 변수는 다수의 쓰레드가 이 변수의 일부 인스턴스를 참조할 때만 공유된다.
12.4.1 쓰레드 메모리 모델
- 동시성 쓰레드의 풀은 한 개 프로세스의 컨텍스트에서 돌아간다. 각각의 쓰레드는 자신만의 별도의 쓰레드 컨텍스트를 가진다. 각 쓰레드는 나머지 프로세스 컨텍스트를 다른 쓰레드와 공유한다. 여기에는 전체 사용자 가상 주소공간이 포함된다. 또한 동일한 오픈된 파일들을 공유한다.
- 동작적인 측면에서, 하나의 쓰레드가 다른 쓰레드의 레지스터를 읽거나 쓰는것은 불가능한 반면, 모든 쓰레드는 공유 가상메모리 내의 모든 위치에 접근할 수 있다. 만일 어떤 쓰레드가 한 메모리 위치를 수정하면, 그 위치를 읽는 다른 모든 쓰레드는 결국 이 변경사항을 알 수 있게된다. 그래서 레지스터들은 절대 공유되지 않지만, 가상메모리는 항상 공유된다.
- 별도의 쓰레드 스택을 위한 메모리 모델은 깔끔하지 않다. 이 스택들을 가상 주소공간의 스택 영역에 포함되어 있고, 대개 이들 각각의 쓰레드에 의해 독립적으로 접근된다. 항상이 아닌 대개라고 하는 이유는 서로 다른 쓰레드 스택이 다른 쓰레드로부터 보호되지 않기 때문이다. 그래서 만일 어떤 쓰레드가 다른 쓰레드의 스택을 가리키는 포인터를 획득하게 된다면, 이 스택의 모든 부분을 읽고 쓸 수 있다.
12.4.2 변수들을 메모리로 매핑하기
- 쓰레드를 사용하는 C프로그램의 변수들은 이들의 저장클래스에 따라 가상메모리에 매핑된다.
- 전역변수
- 전역변수는 함수 밖에서 선언된 모든 변수를 말한다. 런타임에 가상메모리의 읽기/쓰기 영역은 쓰레드에 의해 참조될 수 있는 각각의 전역변수의 정확히 한 개의 인스턴스를 포함한다.
- 지역 자동 변수
- 지역 자동 변수는 함수 내에서 static특성 없이 선언된다. 런타임에 각 쓰레드의 스택은 자신만의 지역 자동 변수의 인스턴스를 가진다. 이것은 심지어 다수의 쓰레드가 동일한 쓰레드 루틴을 사용하는 경우에도 그렇다.
- 지역 정적 변수
- 지역 정적 변수는 함수 안에서 static 특성으로 선언된 변수다. 전역변수 처럼 가상메모리의 읽기/쓰기 영역은 프로그램에서 선언된 각 지역 정적 변수의 정확히 한 개의 인스턴스를 포함한다.
12.4.3 공유변수
- 어떤 변수 v는 자신의 인스턴스중의 한 개가 하나 이상의 쓰레드에 의해 참조되는 경우에만 공유되어 있다고 말한다.
12.5 세마포어로 쓰레드 동기화하기
- 공유변수들은 편리하지만 심각한 동기화 오류를 가져올 수 있다.
- 일반적으로, 운영체제가 여러분의 쓰레드를 위해서 정확한 순서를 선택하게 될지 여부를 예측할 수 있는 방법은 없다. 이러한 부정확성을 갖는 인스트럭션 순서를 진행그래프라고 하는 도구를 이용해서 명확히 할 수 있다.
12.5.1 진행그래프(Progress Graph)
- 진행그래프는 n개의 동시성 쓰레드를 n차원 직교 좌표공강을 지나는 궤적으로 모델링한다. 각각의 축 k는 쓰레드 k의 진행에 대응된다. 각각의 점은 쓰레드 k가 인스트럭션 Ik를 완료한 상태를 나타낸다. 그래프의 원점은 초기 상태를 나타내며, 쓰레드 모두가 아직 한 개의 인스트럭션도 완료하지 못한 상태이다.
- 진행그래프는 인스트럭션의 실행을 하나의 상태에서 다른 상태로의 전환으로 모델링한다. 전환은 한 점에서 인접하는 한 점으로의 화살표로 표시한다. 합법적인 전환은 오른쪽으로 이동하거나 위로 이동하는 경우다. 두 인스트럭션은 동시에 완료할 수 없다. 프로그램이 뒤로 실행되는 경우는 없으며, 따라서 아래로 또는 왼쪽으로 이동하는 것은 규칙에 위배된다.
- 쓰레드 i에 대해서 공유변수의 내용을 조작하는 인스트럭션(Li, Ui, Si)는 크리티컬 섹션을 형성하며 다른 크리티컬 섹션과 중첩되면 안된다. 각각의 쓰레드가 자신의 크리티컬 섹션 내의 인스트럭션들을 실행하는 동안에는 상호 배타적으로 공유변수를 접근하도록 보장하기를 원한다. 이 현상을 상호배제(mutual exclusion)라고 한다.
- 진행그래프에서, 두 개의 크리티컬 섹션의 교차점은 위험영역이라는 상태공간의 구역을 정의한다. 위험영역은 둘레에 있는 상태들과 접해있지만 포함하지는 않는다는 점에 유의해라. 위험영역을 둘러싼 궤적은 안전궤적이라고 한다. 역으로, 위험영역의 어떤 부분이라도 닿은 궤적은 위험궤적이다.
- 모든 안전궤적은 공유 카운터를 정확히 갱신하게 된다. 쓰레드 프로그램을 정확히 실행하도록 보장하려면 쓰레드들을 어떤 방식으로든 동기화해서 이들이 항상 안전궤적을 가지도록 해야한다. 고전적인 방법은 세마포어 개념에 기초한다.
12.5.2 세마포어
- 세마포어 s는 비음수 정수값을 갖는 전역변수로 두 개의 특별한 연산인 P,V를 통해서만 조작할 수 있다.
- P(s) : s가 0이 아니면 P는 s를 감소시키고 즉시 리턴한다. 만일 s가 0이면 이 쓰레드는 s가 0이 아닌 값을 가지고, 쓰레드가 V연산에 의해 재시작될 때까지 정지된다. 재시작후에 P연산은 s를 감소시키고 제어를 호출자에게 돌려준다.
- V(s) : V연산은 s를 1증가 시킨다. 만일 s가 0이 아닌 값이 되는것을 기다리면서 P연산에서 멈춰있는 쓰레드가 있다면, V연산은 이중에서 정확히 한 개의 쓰레드를 시작하고, 그 후에 s를 감소시켜서 자신의 P연산을 완료한다.
- P에서 테스트와 감소연산은 일단 세마포어 s가 0이 아니면 s의 감소가 중단없이 일어난다는 의미에서 개별적으로 일어난다. V에서 증가연산 또한 개별적으로 일어나는데, 그것은 이 연산이 세마포어를 중단없이 로드하고, 증가하고, 저장하기 때문이다. V의 정의가 기다리고 있는 쓰레드들이 재시작되는 순서를 정의하지 않는다는 것에 주목하라. 유일한 요구사항은 V가 정확히 한 개의 대기하는 쓰레드를 재시작해야 한다는 것이다. 그래서 여러개의 쓰레드가 하나의 세마포어를 기다리고 있을 때, 어떤것이 V의 결과로 재시작되는지는 예측 할 수 없다.
- P와 V의 정의는 돌고 있는 프로그램이 적절히 초기화된 세마포어가 음수 값을 가지는 상태로 절대 들어갈 수 없도록 보장해준다. 이 특성을 세마포어 불변성이라고 하며, 동시성 프로그램의 궤적을 제어하기 위한 강력한 도구를 제공한다.
12.5.3 상호배제를 위한 세마포어 이용하기
- 공유변수들을 상호배타적으로 접근하기 위한 방법. 하나의 세마포어 s를 초기값 1로 시작해서 각각의 공유변수에 연계하고 그 후에 대응하는 크리티컬 섹션을 P(s)와 V(s) 연산으로 둘러싸는 것.
- 이런 방법으로 공유변수들을 보호하기 위해 이용된 세마포어는 바이너리 세마포어라고 부르며, 그 이유는 이들의 값이 항상 0 또는 1이기 때문이다. 상호 배타성을 제공하는 목적의 바이너리 세마포어는 뮤텍스(mutex)라고도 한다. 뮤텍스는 P연산을 수행하는 것을 뮤텍스를 잠근다(locking)라고 한다. 비슷하게 V연산을 수행하는 것을 뮤텍스를 연다(unlocking)라고 한다. 뮤텍스를 잠갔으나 아직 열지 못한 쓰레드는 뮤텍스를 소유하고 있다고 말한다.
- 가능한 자원들의 집합에 대한 카운터로 이용된 세마포어는 카운팅 세마포어라고 부른다.
12.5.4 세마포어를 이용한 공유 자원 스케줄링하기
- 세마포어의 또 다른 중요한 용도는 상호배제를 제공할 뿐만 아니라, 공유자원으로의 접근을 스케줄링하는 것이다. 이 시나리오에서, 쓰레드는 세마포어 연산을 이용해서 프로그램 상태의 어떤 조건이 참이 되었다는 것을 다른 쓰레드에 알려준다. 두 개의 고전적이고 유용한 사례는 생산자 - 소비자와 reader - writer 문제다.
생산자 - 소비자 문제
- 생산자와 소비자 쓰레드는 n개의 슬롯을 갖는 제한된 버퍼를 공유한다. 생산자 쓰레드는 반복적으로 새 아이템을 만들고 이들을 버퍼에 삽입한다. 소비자 쓰레드는 반복적으로 아이템을 버퍼에서 제거하고 그 후에 이들을 소모한다(이용한다).
- 아이템들을 추가하고 제거하는 것이 공유변수들의 갱신과 관련되어 있으므로 버퍼에 접근할 때 상호배타성을 보장해야 한다. 그러나 상호배타성을 보장하는 것으로는 충분하지 않다. 버퍼로의 접근을 스케줄링 할 필요도 있다. 만일 버퍼가 차 있으면(빈 슬롯이 없으면), 생산자는 슬롯이 가능해질 때 까지 기다려야 한다. 비슷하게 버퍼가 비어있으면(가용한 아이템이 없다면) 소비자는 아이템이 가용할 때까지 기다려야 한다.
읽기 - 쓰기 문제
- 읽기 - 쓰기 문제는 상호 배제 문제의 일반화된 형태다. 동시성 스레드들의 집합은 메인메모리의 자료구조나 디스크 상의 데이터베이스 같은 공유객체에 접근하고 있다. 일부 쓰레드들은 객체를 읽기만 하고, 다른 쓰레드들은 이것을 수정만 한다. 객체를 읽기만 하는 쓰레드를 reader, 수정만 하는 쓰레드를 writer라고 한다. writer는 객체로 배타적인 접근을 해야 하지만, reader는 이 객체를 무수히 많은 다른 reader들과 공유할 수도 있다.
- reader - writer 문제는 몇 가지 변화가 있을 수 있는데, 이들 각각은 reader와 writer의 우선순위에 근거를 두고 있다. reader를 선호하는 reader - writer 문제는 어떠한 reader도 writer가 이미 이 객체를 이용하도록 허가하지 않았으면 계속 기다려서는 안된다. 두 번째는 writer를 선호하는, writer가 쓸 준비가 되었다면 가능한 한 빨리 자신의 쓰기 작업을 수행하는 것을 요구한다.
- 두 개의 reader - writer 문제에 대한 정확한 해결책은 기아문제로 이어질 수 있으며, 이 경우 쓰레드는 영원히 정지하고 진행하지 못한다.
- 12.6 병렬성을 위해서 쓰레드 이용하기
- 모든 프로그램의 집합은 중첩되지 않도록 순차적, 동시성 프로그램으로 나눌 수 있다. 순차 프로그램은 단일 논리흐름으로 작성 할 수 있다. 동시성 프로그램은 다수의 동시성 흐름으로 작성할 수 있다. 병렬 프로그램은 다중 프로세서에서 돌아가는 동시성 프로그램이다. 그래서 병렬 프로그램의 집합은 동시성 프로그램 집합의 부분 집합이다.
- 서로 다른 쓰레드들에 작업을 할당하는 가장 직접적인 접근방법은 이 배열을 t개의 중첩되지 않은 영역으로 나누고, 그 후에 t개의 서로 다른 쓰레드 각각을 자신의 영역에서 동작하도록 할당한다.
- 메인 쓰레드는 고유의 쓰레드 ID를 각각 피어쓰레드로 전달한다. 각각의 피어쓰레드는 자신의 쓰레드 ID를 사용해서 자신이 작업해야 할 배열의 부분을 결정한다. 이와같이 작은 고유 쓰레드 ID를 피어쓰레드로 보내는 아이디어는 많은 병렬 응용에서 사용되는 일반적인 기술이다.
- 동기화 오버헤드는 비싸고, 가능하면 피해야한다. 만일 피할 수 없다면, 오버헤드는 가능한 한 유용한 계산만큼 축소되어야 한다.
12.7 다른 동시성의 이슈
12.7.1 쓰레드 안정성
- 쓰레드로 프로그램 할 때, 쓰레드 안정성이라고 부르는 특성을 가지는 함수를 작성하도록 유의해야한다.
- 어떤 함수는 다수의 동시성 스레드로부터 반복적으로 호출될 때 항상 정확한 결과를 만드는 경우에만 쓰레드 - 안전 이라고 한다. 만일 어떤 함수가 쓰레드 - 안전하지 못하다면 이것은 쓰레드 - 위험이라고 한다.
- 쓰레드 - 위험한 함수의 네 가지 클래스
- 클래스1
- 공유변수를 보호하지 않는 함수들 이 함수는 보호되지 않은 전역 카운터 변수를 증가시킨다. 이 쓰레드 - 위험한 함수의 클래스는 비교적 쓰레드 - 안전하게 만들기 쉽다 : 공유변수를 P와 V같은 동기화 연산으로 보호한다. 한 가지 장점은 호출하는 프로그램에는 아무 변경도 할 필요가 없다는 것이다. 한 가지 단점은 동기화 연산들이 이 함수를 느리게 할 것이라는 점이다.
- 클래스2
- 다중호출에 대해서 상태를 유지하는 함수들 rand 함수는 쓰레드 - 위험한데, 그 이유는 현재 호출의 결과가 이전 반복 실행으로부터의 중간 결과에 의존하기 때문이다.
- rand 같은 함수를 쓰레드 안전하게 만드는 유일한 방법은 이 함수를 재작성해서 static 데이터를 전혀 사용하지 않도록 하는 것이며, 이를 위해서는 호출자가 상태정보를 인자들로 전달해야한다. 단점은 프로그래머가 이제는 호출하는 루틴에서도 코드를 변경해야 한다는 것이다. 잠재적으로 수 백개의 서로 다른 호출 위치가 존재하는 큰 규모의 프로그램에서 이와 같은 수정을 하는 것은 간단한 일이 아니며 에러가 발생하기 쉽다.
- 클래스3
- 정적변수를 가리키는 포인터를 리턴하는 함수. 만일 이러한 함수를 동시성 쓰레드로부터 호출한다면 재앙이 발생할 수 있으며, 그 이유는 한 개의 쓰레드가 사용하는 결과들이 다른 쓰레드에 의해 조용하게 덮어써지기 때문이다.
- 이 클래스의 쓰레드 - 위험 함수들을 다루는데 두 가지 방법이 있다. 한 가지 옵션은 함수를 다시 작성해서 호출자가 결과를 저장하는 변수의 주소를 전달하는 것이다. 이렇게 하면 모든 공유 데이터를 없앨 수 있지만, 이를 위해서는 프로그래머가 함수의 소스 코드에 접근할 수 있어야 한다.
- 만일 쓰레드 - 위험 함수가 수정하기 어렵거나 불가능하다면(ex : 코드가 매우 복잡하거나 소스코드가 없는 경우), 다른 옵션은 lock-and-copy 기술을 이용하는 것이다. 기본 아이디어는 뮤텍스를 쓰레드 - 위험 함수와 연계하는 것이다. 각각의 호출 위치에서 뮤텍스를 잠그고, 쓰레드 - 위험 함수를 호출하며, 함수가 리턴한 결과를 사적 메모리 위치로 복사하고, 그 후에 뮤텍스를 풀어준다. 프로그래머는 호출자의 변경을 최소화 하기 위해 lock-and-copy를 수행하는 쓰레드 안전한 래퍼함수를 정의해야 하며, 쓰레드 - 위험 함수로의 모든 호출을 이 래퍼로의 호출로 대체한다.
- 클래스4
- 쓰레드 - 위험 함수를 호출하는 함수들. 만일 어떤 함수 f가 쓰레드 - 위험 함수 g를 호출한다면 f는 쓰레드 - 위험한가? → 그때 그때 다르다. 만일 g가 다수의 호출에 걸쳐서 상태에 의존하는 클래스2의 함수라면 f도 쓰레드 위험이며 g를 재시작하는 것 말고는 다른 방도가 없다. 그렇지만 만일 g가 클래스1 또는 클래스3 함수고, 프로그래머가 호출 위치와 다른 결과로 생성되는 공유데이터를 뮤텍스로 보호한다면 f는 여전히 쓰레드 안전이다.
- 클래스1
12.7.2 재진입성
- 재진입(reentrancy) .가능한 함수라고 하는 쓰레드 - 안전 함수의 중요한 클래스가 있으며, 이들은 다수의 쓰레드에 의해 호출될 때 공유데이터는 전혀 참조하지 않는 특성으로 규정된다. 비록 쓰레드 - 안전과 재진입 가능이라는 용어가 유사어로 종종 사용될지라도(부정확하게) 여기에는 준수할 가치가 있는 명확한 기술적 구분이 존재한다.
- 모든 함수들의 집합은 쓰레드 - 안전과 쓰레드 - 위험 함수들의 중첩되지 않은 집합으로 나누어진다. 재진입가능 함수들의 집합은 쓰레드 - 안전 함수들의 적절한 부분집합이다.
- 재 진입 가능 함수들은 대개 재진입 불가능 쓰레드 - 안전 함수들보다 더 효율적인데, 그 이유는 이들이 동기화 연산을 필요로 하지 않기 때문이다. 추가로, 클래스2 쓰레드 - 위험 함수를 쓰레드 - 안전 함수로 변환하는 유일한 방법은 이 함수를 다시 작성해서 재진입 가능하게 만드는 것이다.
- 일부 함수의 코드를 검사하고 사전에 이것이 재진입 가능인지 선언하는 것이 가능할까? 불행하게도 상황에 따라 다르다. 만일 모든 함수 인자가 값으로 전달되고(x포인터), 모든 데이터 참조가 지역 자동 스택 변수들로 이루어진다면 이 함수는 명시적으로 재진입 가능이며, 이것이 우리가 이 함수가 재진입성을 이것이 어떻게 호출되었는지에 관계없이 선언할 수 있다는 의미에서 그렇다.
- 하지만 만일 우리의 가정을 약간 완화하고, 그렇지 않으면 명시적으로 재진입 가능한 함수에서 일부 매개변수들을 참조 형태로 전달되게 해주며, 그 후에 만일 호출하는 쓰레드들이 포인터를 공유되지 않은 데이터로 조심스럽게 전달하려고 한다면, 이것이 유일하게 재진입 가능하다는 의미에서 간접적으로 재진입 가능 함수를 가진다. 우리는 재진입 가능이라는 용어를 명시적이고 묵시적인 재진입 가능한 함수들 모두를 포함하기 위해서 항상 사용한다. 그렇지만, 재진입성은 때로는 호출자와 피호출자 모두의 특징이며 피호출자만을 위한것은 아니다.
12.7.3 쓰레드 프로그램에서 기존 라이브러리 함수 사용하기
- 대부분 리눅스 함수들은 표준 C라이브러리에서 정의된 함수를 포함해서 일부 예외를 제외하고는 쓰레드 - 안전하다.
- 몇 함수는 쓰레드 - 위험한데 rand와 strtok을 제외하고, 이들은 클래스3의 변형된 형태이며, 이들은 정적변수로의 포인터를 리턴한다. 만일 쓰레드를 이용하는 프로그램에서 이중 하나의 함수를 호출할 필요가 있다면, 호출자에게 가장 덜 피해를 주는 방법은 lock-and-copy 방식이다. 그렇지만 lock-and-copy 방식은 많은 단점이 있다. 첫째, 추가적인 동기화 과정 때문에 프로그램 속도가 느려진다. 둘째, 복잡한 구조체의 구조체를 가리키는 포인터를 리턴하는 함수들은 전체 구조체 계층구조를 복사하기 위해서는 구조체 가장 말단까지 복사해야한다. 셋째, lock-and-copy 방식은 호출들에 대해서 정적 상태의 의존하는 rand 같은 클래스2 쓰레드 - 위험 함수들에 대해서는 동작하지 않는다.
12.7.4 경쟁상대
- 경쟁(race)은 프로그램의 정확성이 다른 쓰레드가 y지점에 도착하기 전에 자신의 제어흐름에서 x지점에 도착하는 하나의 쓰레드에 의존할 때 일어난다. 경쟁은 대개 프로그래머들이 쓰레드가 실행상태 공간을 지나가는 어떤 특정궤적을 따른다고 가정하며, 쓰레드 프로그램이 모든 가능한 궤적에 대해서도 정확하게 동작해야 한다는 불문율을 잊어버리기 때문에 일어난다.
12.7.5 교착상태
- 세마포어는 교착상태(deadlock)라고 부르는 못된 종류의 잠재적인 런타임 에러를 유발하며, 이것은 다수의 쓰레드가 절대로 참이 될 수 없는 조건을 기다리면서 정지되어있는 경우를 말한다. 진행그래프는 교착상태를 이해하기 위한 소중한 도구다
- 프로그래머가 P와 V연산을 잘못 배치해서 두 개의 세마포어를 위한 금지된 구역이 겹치게 된다. 만일 일부 실행 궤적이 교착상태 d에 도달하게 되었다면, 더 이상의 진행은 불가능한데, 그 이유는 겹치는 금지 구역이 모든 합법적인 방향으로의 진행을 막기 때문이다.
- 겹치는 금지구역은 교착구역이라고 부르는 상태들의 집합을 만든다. 만일 어떤 궤적이 교착구역 내의 상태와 닿는다면 교착상태는 피할 수 없다. 궤적들은 교착구역에 진입할 수 는 있지만, 결코 떠날 수는 없다.
- 교착 상태는 언제나 예측 가능한 것이 아니므로 특히 어렵다. 프로그램이 어떤 머신에서는 잘 돌다가도 다른 머신에서는 교착상태에 빠질 수 있다. 가장 나쁜경우는 서로 다른 실행들이 서로 다른 궤적을 가지기 때문에 에러가 반복되지 않는다는 점이다.
728x90
'책 > CSAPP' 카테고리의 다른 글
CSAPP 10 (0) | 2024.02.24 |
---|---|
CSAPP 12.6 - 12 (0) | 2024.02.21 |
CSAPP 12.4 - 12.5 (0) | 2024.02.20 |
CSAPP 12-12.3 (0) | 2024.02.19 |
CSAPP 11 (1) | 2024.02.18 |