경쟁(Race)는 프로그램의 정확성이 다른 쓰레드가 포인트 y에 도달하기 전에 제어 흐름에서 한 쓰레드가 포인트 x에 도달하는 것에 의존할 때 발생한다.
예제를 사용하면 레이스의 본질을 이해하기 쉽다. 아래 코드에서는 4 개의 피어 쓰레드를 생성하고 고유한 정수 ID의 포인터를 각각의 쓰레드에 넘겨준다.
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define N 4
void *thread(void *vargp);
int main()
{
pthread_t tid[N];
int i;
for (i = 0; i < N; i++) // Line 11
pthread_create(&tid[i], NULL, thread, &i); // Line 12
for (i = 0; i < N; i++)
pthread_join(tid[i], NULL);
exit(0);
}
/* Thread routine */
void *thread(void *vargp)
{
int myid = *((int *)vargp); // Line 21
printf("Hello, world! It's me, thread #%d!\n", myid);
return NULL;
}
각 피어 쓰레드는 자신의 인자로 넘어온 ID를 지역 변수에 복사하고 자신의 ID에 포함하는 메세지를 인쇄한다. 매우 간단한 것처럼 보이지만, 이 프로그램은 다음과 같은 잘못된 결과를 얻게 된다.
unix> ./race
Hello, world! It's me, thread #1!
Hello, world! It's me, thread #3!
Hello, world! It's me, thread #2!
Hello, world! It's me, thread #3!
문제는 각각의 피어 쓰레드와 메인 쓰레드 간의 경쟁 때문에 발생한다.
메인 쓰레드가 쓰레드를 만들 때, 로컬 스택 변수의 i 포인터를 전달한다. 이때, 레이스는 11번 줄에서의 다음 번 i의 증가와 21 번 줄에서 vargp의 역참조 및 할당 사이에서 발생한다. 만일 12 번 줄에서 메인 쓰레드가 실행되기 이전에 피어 쓰레드가 21번 줄을 실행한다면 myid 변수는 정확한 ID를 얻을 것이다. 그렇지 않다면 다른 쓰레드의 ID를 가져오게 된다. 즉, 피어 쓰레드가 얼마나 빨리 생성되느냐에 따라 전달되는 i 값이 변화한다.
이러한 버그는 커널이 쓰레드의 실행을 어떻게 스케쥴링하는지에 달렸다. 특정 시스템에서는 추적이 가능하지만 또 다른 시스템에서는 정상적으로 동작할 수도 있기에 레이스 문제는 까다롭다.
레이스를 제거하기 위해서는 공유 변수를 통해 상태를 전달하는 대신, 각 쓰레드마다 고유한 공간을 할당할 수 있다.
int main()
{
pthread_t tid[N];
int i *ptr;
for (i = 0; i < N; i++)
ptr = malloc(sizeof(int));
*ptr = i;
pthread_create(&tid[i], NULL, thread, ptr);
for (i = 0; i < N; i++)
pthread_join(tid[i], NULL);
exit(0);
}
/* Thread routine */
void *thread(void *vargp)
{
int myid = *((int *)vargp);
printf("Hello, world! It's me, thread #%d!\n", myid);
return NULL;
}
이렇게 되면, 쓰레드의 실행 순서가 꼬이더라도 각자 고유한 ID를 갖게 되므로 앞서 제시한 레이스 문제를 해결할 수 있다.
'CS > OS' 카테고리의 다른 글
동기화 (3) Thread Safety (0) | 2022.12.07 |
---|---|
동기화 (2) 세마포어 (0) | 2022.12.07 |
동기화 (1) (0) | 2022.12.07 |
쓰레드 (1) | 2022.12.07 |
프로세스를 이용한 동시성 프로그래밍 (0) | 2022.12.07 |