×
Crocus
공부한 내용을 정리하는 블로그로 시작한
Crocus는 2014년 1월 14일 부터 시작하여
현재 월 6만명, 총 2,045,665명의 방문자 수를 기록하고 있습니다.
Donation
이제 많은 사용자들이 이용하는 만큼
더 다양한 서비스 개발/제공을 위해 후원금을 모금하고자 합니다.
후원을 해주시는 분들은 Donators 명단에 성명, 후원금을 기입해드리며
Crocus 블로그가 아닌 다른 곳에 정리해둔 저만의 내용을 공유해 드리고자 합니다.
Account
예금주 : 고관우
신한은행 : 110-334-866541
카카오뱅크 : 3333-01-7888060

👉 후원 페이지 바로가기 Donators
익명 : 5000원(Crocus응원합니다.)
busyhuman: 5000원(유용한 지식 감사합니다.)
익명 : 5000원(알고리즘 학습러)
반응형

- 본 내용은 Linux (Ubuntu 14.04 lts)를 기반으로 제작되었습니다. -




목록

1. 스레드와 프로세스의 공통점 및 차이점

2. 스레드 이용 방법(Posix Thread 기준)

3. 메모리 쉐어링(Memory Sharing)

4. Unsafe Region





1. 스레드와 프로세스의 공통점 및 차이점


네트워크 프로그래밍을 하기 위해서는 스레드가 무엇인지, 프로세스가 무엇인지, 

그에따른 스레드 및 프로세스의 차이점을 알고 있어야 한다.




스레드란 무엇인가?


스레드는 하나의 프로세스가 있을 때 생성할 수 있는 또 다른 작은 프로세스이다.


사실 프로세스또한 스레드이다. 이를 메인 스레드라고 한다.


스레드는 프로세스의 부분이고 프로세스의 리소스를 공유할 수 있다.




리소스란 무엇인가?


리소스라는 개념은 프로세스가 생성될 때 OS로 부터 할당받은 주소 공간(즉, 메모리 할당)이라 생각해도 무방하다.


이 주소공간은 Text, Data, Heap, Stack으로 구성되어있고, 


이 중 스레드는 메인 스레드의 스택을 제외한 모든 것을 다 이용할 수 있다.


조금 더 구체적으로 말하자면,


메인 스레드가 가지고 있던 라이브러리, 힙, data(bss), text(code)를 이용할 수 있게된다.



이때 프로세스(앞으로는 메인 스레드라고 말하겠다.)와 스레드 모두 


OS의 스케줄러에 의해 컨텍스트 스위칭을 당하게 되고 자신에게 CPU 리소스가 할당될 때 돌게된다.


이를 이용하여 Concurrent Programing을 할 수 있게 된다.  



왜 스레드를 만드는가?


사실 스레드를 만드는 이유가 가장 중요한 요소이다.


스레드를 만드는 이유는 만약 하나의 프로그램에서 여러가지 행동을 동시에 해야될 상황이 있는데


프로그램에 하나의 프로세스만 있다면(하나의 메인 스레드) 동시 수행을 하고싶어도 메인 스레드가 보고있는


코드 위치가 한군데 밖에 지정되지 않기 때문에 동시에 무엇인가를 진행 할 수 없다.


예를들어보자, 서버 클라이언트가 있는데, 서버가 어셉트를 기다리는 동안 다른 일도 하고 싶지만, 


어셉트에 걸리는 순간 어셉트를 기다리고만 있게되어 다른 작업을 수행할 수 없게된다.


이때 스레드라는 개념이 존재한다면 메인 스레드는 어셉트를 기다리고 나머지 스레드는 다른 일을 할 수 있게된다.







스레드가 공유하지 않고 가지고 있는 것


스레드는 위의 그림에서 보듯이 스레드는


Thread ID, Control flow, Data register, Condition codes, Stack Pointer(SP), Program Counter(PC) 및 스레드 스택을 가지고 있다.


프로세스가 공유하지 않고 가지고 있는 것 또한 위의 내용과 같다.


왜냐? 프로세스 또한 메인 스레드이기 때문이다.



스레드와 프로세스의 공통점은 무엇이 있을까?


스레드와 프로세스의 공통점은 다음과 같다.


1. 각자의 ID를 가지고 있다.

2. 각자의 control flow를 가지고 있다.

3. 각자의 스택과 Stack Pointer을 가지고 있다.

4. 각자 동시에 진행 할 수 있게된다.

5. 컨택스트 스위칭이라는 개념에 의해 CPU를 점유하냐, 못하냐가 정해진다.



스레드와 프로세스의 차이점은 무엇이 있을까?


1. 스레드는 프로세스의 자원을 쉽게 공유 할 수 있다.

(이때 프로세스 또한 프로세스끼리 자원을 공유 할 수있는데 IPC라는 것을 통해야하며, 매우 복잡한 과정이다.


2. 스레드는 프로세스보다 더 가볍다.

즉, 만들고 지우기가 쉽다.


왜냐?? 스레드는 프로세스에 있는 것을 공유를 하지 그것(text, data, heap ...)을 생성하지 않기때문이다.


이때 멀티 프로세싱 혹은 멀티 스레딩 하나만 골라야 하는 상황이라면


각 프로세스의 주소 영역은 보호되는 독립적인 영역이니까 안정성이나 보안면에서 멀티 프로세스가 유리할것이고

디버깅이 중요시 여겨지는 프로그래밍이라면 멀티 프로세스가 유리할 것이다.


반면 데이터와 코드 세그먼트를 포함한 메모리 공간이 공유되니까 성능이 중요한거면 멀티 스레드가 좀 더 유리하다.



2. 스레드 이용 방법(Posix Thread 기준)



스레드 생성


int pthread_create(pthread_t* threadID, const pthread_arr_t* arrtribute, void *startRoutine, void *argumentPointer);


threadID :: 스레드의 ID가 저장되는 변수이다.

attribute :: 스레드에 특성을 부여하는 것인데 보통 NULL을 준다.

startRoutine :: 스레드가 작동 할 코드를 알려준다.

argumentPointer :: 스레드에 보내 줄 인자를 설정한다.


return 값 :: 0이면 성공, 그 외 실패 혹은 에러



스레드 조인(Join)


특정 상황에서 메인 스레드가 자식 스레드가 도는 동안 멈춰 있어야 할 경우가 있을 수 있다.

이때 Join을 이용하여 스레드의 동작 순서를 조절 해 줄 수 있다.


int pthread_join(pthread_t* threadID, void** returnValuePointer);


threadID :: 해당하는 threadID에 대한 자식 스레드가 종료 될 때 까지 기다린다.

returnValuePointer :: 자식 스레드가 종료될 때 return해주는 값을 받아오는 역할을 한다.


return 값 :: 0이면 성공, 그 외 실패 혹은 에러


스레드 create와 join으로 이루어진 예제 :: http://www.crocus.co.kr/484



스레드 Detach


스레드의 조인 없이 동시 병렬적으로 순서에 무관하게 움직이게 하고 싶다면 어떻게 해야하나?


이때는 Detach를 통해 프로세스와 스레드가 떨어져 행동하도록 하면 된다.


Q. 그렇다면 Join도 안쓰고 Detach도 쓰지 않고 그냥 Create만 하면 컨택스트 스위칭에 의해 동시에 아무거나 움직이지 않나??


A. 맞는 말이다. 하지만, 메인 스레드가 종료되면 자식 스레드를 현대 OS에서는 자동으로 죽이고 처리해주지만,

호환성면에서 OS를 믿어선 안되고, 정석적으로 코딩 하는 습관이 좋다.



int pthread_detach(pthread_t threadID);


threadID :: 메인 스레드에서 떼어 낼 스레드의 ID를 입력한다.


return 값 :: 0이면 성공, 0이 아니면 에러 혹은 실패


스레드를 떼어내게 되면, 메인 스레드는 기다리지 않고 돌게된다.


Detach된 스레드가 죽게되면 메인 스레드에서 처리하지 않고, OS에서 자동적으로 처리하게 된다.


스레드 detach 예제 :: http://www.crocus.co.kr/484의 5번 예제





3. 메모리 쉐어링(Memory Sharing)



메모리 쉐어링이란?


스레드는 이전에 말했듯이 프로세스의 메모리를 공유 할 수 있다.


우리가 흔히 코드에서 사용했던 메모리 쉐어링의 예제를 살펴보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <pthread.h>
 
int k = 2;
 
void *threadfunc(void *argument)
{
    k = 3;
    printf("메모리 쉐어링이 가능하기에 k를 이용 가능\n");
    
    return 0;
}
int main()
{
    pthread_t TID;
 
    int n = 1;
 
    printf("k 는 전역 변수로써 메모리 쉐어링이 가능하다. :: %d\n",k);
    printf("n은 지역 변수로써 메인 스레드는 쓸 수 있지만\n");
    printf("다른 스레드는 쓸 수 없는 메모리 쉐어링이 불가능 한 것입니다. :: %d",n);
 
    pthread_create(&TID, NULL, threadfunc, NULL);
 
    return 0;
}
 
//                                                       This source code Copyright belongs to Crocus
//                                                        If you want to see more? click here >>
Crocus


이처럼 전역 변수는 data 영역에 들어있기에 스레드와 메인 스레드간에 쉐어링이 가능하다.


즉, 스레드는 그들 프로세스가 가진 가상 메모리를 공유 할 수 있다.


하지만, 스레드는 각자의 스택은 공유 할 수 없다.

(아까도 말했듯이, 프로세스 즉, 메인도 메인 스레드이기에 스택은 독립적이다.)


** 한가지 더 **

같은 스레드 함수를 쓰는 스레드간 어떤 변수를 공유하고 싶다면 static 변수를 이용하면 스레드간 공유가 가능하다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <pthread.h>
 
void *threadfunc(void *argument)
{
    static int val = 1;
    printf("스레드간 메모리 쉐어링을 가능하게 하는 static 변수 :: %d\n",val);
    
    return 0;
}
int main()
{
    pthread_t TID1, TID2;
 
    pthread_create(&TID1, NULL, threadfunc, NULL);
    pthread_create(&TID2, NULL, threadfunc, NULL);
 
    return 0;
}
 
//                                                       This source code Copyright belongs to Crocus
//                                                        If you want to see more? click here >>
Crocus





위의 그림을 보자.


1. 전역변수 globalVariable은 Data영역에 들어가있는 것을 볼 수 있다.


2. static으로 선언된 스레드 내부 변수도 Data영역에 존재한다.(하지만 하나의 threadFunc를 호출 한 스레드끼리 공유가 가능하다.)


3. localVariable은 스레드 각자의 스택에 쌓여있다.


4. 메인의 localVariable와 localPointer은 메인 스레드의 스택에 쌓여있고, malloc을 통해 생성한 것은 heap에 쌓여있다.



메모리 쉐어링 결과 각 변수에 대해 어떤 스레드들이 쓸 수 있는지 없는지에 대해 알려주는 표이다.


예를들어 staticVariable은 메인 스레드는 쓸 수 없지만, 

하나의 threadFunc를 같이 이용하는 threadDs[0]와 threadDs[1]은 이용할 수 있다. 







4. Unsafe Region


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <pthread.h>
#include <stdio.h>
 
int sum = 0;
 
void *threadRoutine(void *argumentPointer)
{
    int i;
    for(i = 0; i < 1000000; i ++)
        sum++;
    
    return NULL;
}
 
int main()
{
    pthread_t threadID1, threadID2;
 
 
    printf("Create Thread!\n");
    pthread_create(&threadID1, NULL, threadRoutine, NULL);
 
    printf("Create Thread!\n");
    pthread_create(&threadID2, NULL, threadRoutine, NULL);
 
    pthread_join(threadID1, NULL);
    pthread_join(threadID2, NULL);
 
    printf("Result :: %d\n",sum);
 
    return 0;
}
 
//                                                       This source code Copyright belongs to Crocus
//                                                        If you want to see more? click here >>
Crocus



이 코드를 돌리면 정답은 무엇일까? 2000000이 나올까?



위와 같이 나오는 경우도 있겠지만, 나오지 않는 경우가 대부분이다.




우리가 원하는 경우는 위와 같은 경우이다. 즉, 스레드가 1을 더하는동안 절대 컨택스트 스위칭이 되지 않는 형태를 취해야한다.

위와 같은 모습을 이루면 값은 2000000이 나온다.



하지만 현실은 다르다. 컨택스트 스위칭은 언제 어디서 스케줄러가 일으킬 지 모르기에, 

위와 같은 모습을 이루면 값은 2000000이 나오지 않고 엉뚱한 값이 나올 수 있다.


이러한 것이 Unsafe Region이다.


즉, 스레드를 이용하면 우리는 많은 혜택을 볼 수 있다. 가볍기에 만들기, 지우기도 쉽고 Concurrent Programing도 지원해준다.


하지만, 이러한 공유되는 변수에 대해서는 조금 더 민감하게 다루어 줘야 할 필요가 있다.




반응형