쓰레드를 활용하여 프로그래밍을 수행할 때 쓰레드 안전성이라고 부르는 특성을 가지는 함수를 작성하도록 유의해야 한다. 어떤 함수는 다수의 동시성 쓰레드로부터 반복적으로 호출될 경우 항상 정확한 결과를 만드는 경우에만 Thread Safety 라고 부른다. 만일 어떤 함수가 이를 충족하지 못하는 상태라면 이를 Thread-unsafe 하다고 표현한다.
Thread-unsafe한 4가지 클래스를 정의하면 다음과 같다.
- 클래스 1: 공유 변수를 보호하지 않는 함수들. 직전 글에서 볼 수 있듯, 쓰레드 함수 내에서의 공유 변수 접근을 보호하지 못한다면 동기화 오류가 발생할 수 있다. 세마포어 연산 P와 V를 활용하여 Thread-safety한 쓰레드 함수로 개선할 수 있다.
unsigned int next = 1;
/* read - return pseudo-random integer on 0..32767 */
int rand(void)
{
next = next * 1103515245 + 12345;
return((unsigned)(next/65536) % 32768);
}
/* srand - set seed for rand() */
void srand(unsigned int seed)
{
next = seed;
}
- 클래스 2: 다중 호출 사이에서 전역 변수로 상태를 유지하는 함수들. 위의 코드 상 의사 랜덤 숫자 생성기가 그 예시이다. rand 함수는 Thread-unsafe 인데, 그 이유는 현재 호출의 결과가 이전 반복 실행으로부터의 중간 결과에 의존하기 때문이다. srand를 호출하여 seed 값을 가져온 이후에 rand를 한 개의 쓰레드로부터 반복해서 호출한다면 반복되는 숫자들의 배열을 기대할 수 있겠지만, 이 가정은 만일 다수의 쓰레드가 rand를 호출한다면 더이상 성립하지 않는다.
대신 상태를 매개변수를 통해 관리하게 된다면 앞선 문제를 개선할 수 있다. 해당 함수에 제공되는 매개변수는 각 쓰레드의 고유한 스택 영역에 해당하기 때문에 쓰레드 간 공유 문제를 제거할 수 있다. 다음 코드는 그와 같은 해결을 보여준다.
/* read - return pseudo-random integer on 0..32767 */
int rand_r(int *nextp)
{
*nextp = *nextp * 1103515245 + 12345;
return((unsigned)(*nextp/65536) % 32768);
}
매개변수를 도입함에 따라 전역 변수에 의존하여 상태를 의존하지 않게 되었다. 즉 공유 데이터는 전혀 참조되지 않으며 이를 재진입 가능한 함수라고 부르며 그에 따른 속성을 재진입성(Reentrancy)이라고 부른다. 흔히 재진입성과 Thread-safety, Thread-unsafe 와의 관계를 많이 헷갈리는데, 다음 그림을 통해 보다 명확히 이해할 수 있다.
모든 함수는 크게 Thread-safe 한 함수와 Thread-unsafe 함수로 나뉜다. Thread-safety 측면에서 앞서 살펴본 것처럼 동기화를 통해 Thread-safe를 달성할 수 있다. 그러나 애초에 공유 변수에 접근하지 않는 함수들도 Thread Safe 하다고 할 수 있다. 이를 우리는 재진입가능한 함수라고 부르게 된다.
- 클래스 3: 정적 변수를 가리키는 포인터를 리턴하는 함수. ctime과 gethostbyname과 같은 함수들은 static 변수에 결과를 계산하고 그 이후에 이 변수를 가리키는 포인터를 리턴한다. 만일 이러한 함수들을 동시성 쓰레드로부터 호출한다면 재앙이 발생할 수 있다. 그 이유는 한 개의 쓰레드가 사용하는 결과들이 다른 쓰레드들에 의해 조용히 덮어써지기 때문이다.
이를 크게 2 가지 방법으로 대처할 수 있다.
옵션 1. 함수를 다시 작성하여 호출자가 결과를 저장하는 변수의 주소를 전달하도록 변환
이렇게 하면 모든 공유 데이터를 없앨 수 있지만, 이 방법은 프로그래머가 함수의 소스코드를 접근할 수 있다는 것을 가정해야만 한다.
옵션 2. lock-and-copy
만일 Thread-unsafe 함수가 수정하기 어렵거나 불가능하다면, 다른 옵션은 lock-and-copy 기술을 사용하는 것이다. 기본 아이디어는 뮤텍스를 Thread-unsafe 함수와 연계하는 것이다. 각각의 호출 위치에서 뮤텍스를 잠그고, 그 이후 thread-unsafe 함수를 호출하고, 함수가 리턴한 결과를 사적 메모리 공간에 복사하고, 그 이후에 뮤텍스를 풀어준다. 다음의 예시는 앞서 설명한 lock-and-copy를 코드로 표현한 것이다.
char *ctime_ts(const time_t *timep, char *privatep)
{
char *sharedp;
P(&mutex);
sharedp = ctime(timep);
strcpy(privatep, sharedp);
V(&mutex);
return privatep;
}
- 클래스 4: Thread-unsafe 함수를 호출하는 함수들. 만일 어떤 함수 f 가 Thread-unsafe 한 함수 g 를 호출한다면, f는 때에 따라 Thread-unsafe 하다. 만일 g가 다수의 호출을 지나는 상태에 의존하는 클래스 2 함수라면 f는 Thread-unsafe 하다. 만일 g가 클래스 1, 3 함수일 경우, 뮤텍스를 적절히 활용한다면 f 는 여전히 Thread-safe 상태에 머물게 된다.
모든 표준 C 라이브러리는 Thread-safety를 보장한다. 또한 대부분의 Unix 시스템 콜 또한 Thread-safe 를 보장하지만 몇몇 예외가 존재한다.
'CS > OS' 카테고리의 다른 글
동기화 (4) Race (0) | 2022.12.07 |
---|---|
동기화 (2) 세마포어 (0) | 2022.12.07 |
동기화 (1) (0) | 2022.12.07 |
쓰레드 (1) | 2022.12.07 |
프로세스를 이용한 동시성 프로그래밍 (0) | 2022.12.07 |