Shared Variables in Threaded Programs
Concurrent Programming 접근법을 3가지 배웠다.
- Process
Process는 자신만의 주소 공간이 있기 때문에 공유 메모리가 없다.
IPC를 통해서 공유 메모리나 메시지 패싱을 OS의 도움을 받아 Process 간 통신이 가능하다. - Event (I/O Multiplexing)
Epoll이나 Select같은 시스템 콜을 사용해서 Process 하나에 File Descriptor Socket을 배열로 만들어서 Socket에 메시지가 오면 이벤트가 발생했다는 것을 확인할 수 있다. 단일 주소 공간을 가지고 있지만, 공유가 용이했다. - Thread
Thread는 Process내에서 생성 소멸된다.
Context Switch 오버헤드가 적고, Thread Creation과 Reaping 하는 데 오버헤드도 적다.
프로그래머의 관점에서 Thread의 장점 중 하나는 여러 Thread가 같은 프로그램 내의 변수를 공유하는 데 쉽다는 것이다. 그러나, 변수가 공유되는 과정은 매우 복잡하다. Thread 프로그램을 잘 만들기 위해서 공유하는 것이 어떤 의미인지, 어떻게 작동하는지에 대해 명확하게 이해해야 한다.
Thread는 Process의 Stack을 제외한 메모리 공간을 다른 Thread와 공유하기 때문에 변수를 공유하는 것이 쉬울 것이라는 직관이 든다. 그렇다면 프로그램에서는 어떤 변수가 공유되는 것인가?
간단하게 Global 변수들은 Shared 변수들이고 Stack 변수들은 Private 한 변수라고 할 수 있다.
Global 변수들은 Data, Heap 영역을 공유한다. Stack 변수들은 각 Thread마다 가지고 있는 Local 변수들이다.
그렇다면 Stack 변수들은 공유되지 않을까?
프로그램에서 변수가 공유되는지 여부를 이해하기 위해 해결해야 할 기본적인 질문들이 있다.
- Thread의 기본 Memory Model은 무엇인가?
- Memory에 변수의 Instance가 어떻게 매핑되는가?
- Instance 각각을 참조하는 Thread의 수는 얼마나 되는가?
변수는 여러 Thread가 변수의 일부 Instance를 참조하는 경우에만 공유된다.
Threads Memory Model
Conceptual Model
여러 Thread는 Process의 Context를 공유하여 동시에 실행되는데 각 Thread는 Process의 Excution Flow다.
프로그램은 명령어들의 집합이고 이 집합이 실행되는 흐름을 Sequence라고 한다면,
Thread란 Sequence들의 Flow가 여러 개 존재할 수 있으며 Concurrent하게 실행할 수 있다.
한 코드를 실행하는 Thread가 여러 개 있다면, 그 코드의 Sequence들은 각각의 Excution flow가 여러 개 있다고 할 수 있다.
- Thread는 자신만의 Context를 가지고 있다.
- Thread ID, Stack, Stack Pointer, PC, Condition Codes, GP reg...
- 모든 Thread는 나머지 Process Context를 공유한다.
- Code, Data, Heap, Process의 가상 주소 공간의 Shared Library segments
- Open files, File descriptor, Handler...
Operational Model
실제로 작동하는 관점에서 한 Thread가 다른 Thread의 Register를 읽거나 쓰는 것은 불가능하다.
반면, 모든 Thread는 공유 가상 메모리의 모든 곳에 접근할 수 있다.
일부 Thread가 메모리 위치를 수정하는 경우 다른 Thread가 해당 위치를 읽으면 의도하지 않은 결과가 발생할 수 있다.
- Register는 공유되지 않는다.
- 한 Thread가 다른 Thread의 Stack을 읽고 쓸 수 있다.
Example
#include "csapp.h"
#define N 2
void *thread(void *vargp);
char **ptr; /* Global variable */
int main() {
int i;
pthread_t tid;
char *msgs[N] = {
"Hello from foo",
"Hello from bar"
};
ptr = msgs;
for (i = 0; i < N; i++)
Pthread_create(&tid, NULL, thread, (void *)i);
Pthread_exit(NULL);
}
void *thread(void *vargp) {
int myid = (int)vargp;
static int cnt = 0;
printf("[%d]: %s (cnt=%d)\n", myid, ptr[myid], ++cnt);
return NULL;
}
Static 변수는 Data 영역에 할당되고 Thread가 실행되는 동안에 존재한다.
Peer Thread는 Global ptr 변수를 통해 Main Thread의 Stack을 간접적으로 참조한다.
Mapping Variables to Memory
Global variables
- 함수 외부에 선언된 변수
- 가상 메모리에는 Global 변수의 인스턴스가 정확히 하나 포함되어 있다.
Local variables
- static 속성 없이 함수 내에서 선언된 변수
- 각 Thread의 Stack에는 Local 변수의 자체 인스턴스가 포함된다.
Local static variables
- static 속성을 가지고 함수 내에서 선언된 변수
- Global 변수와 마찬가지로 가상 메모리에 Local 변수의 인스턴스가 한 개 포함된다.
Shared Variables
변수 v의 Instance 중 하나가 둘 이상의 Thread에 의해 참조되는 경우에 공유된다고 한다.
예를 들어 위 예제에서 프로그램의 변수 cnt는 Runtime동안 Instance가 하나만 있고 이 Instance는 두 Peer Thread에서 모두 참조할 수 있기 때문에 공유된다. 반면 myid는 두 Instance가 각각 정확히 하나의 Thread에 의해 참조되기 때문에 공유되지 않는다. msgs와 같은 Local 변수는 Thread 간에 간접적으로 공유할 수 있다.