Thread

Seongho·2022년 4월 15일
0

운영체제

목록 보기
4/12

기본

프로세스는 운영체제에 의해 메모리를 할당받아 CPU에서 실행중인 프로그램이고, 프로세스는 데이터와같은 자원, 그리고 스레드로 구성된다.

프로세스의 문제점 인지

  • 프로세스는 address space(program code, data), OS 자원, 하드웨어 상태값(PC, SP, registers..)을 갖고 있어서 이런 process를 많이 만들어 내는 것은 매우 비효율적인 작업이다.
  • OS는 프로세스를 병렬처리하지 않고 스케줄링하여 프로세스를 컨트롤하여야 하고, 프로세스는 각각의 메모리 공간을 갖기 때문에 시간, 공간의 낭비가 많다.
  • 프로세스 간에 communication을 하는 Inter-process communication이 비용이 많이 든다.
    **Inter-process communication: process간에 협력하여 작업을 수행하는 것. 예를 들면, 인터넷에 글을 쓸 때 Network 관련 process와 Keyboard I/O 관련 process의 협력으로 작업이 이루어진다.

쓰레드(Thread)

쓰레드는 프로세스 내에 실제 작업을 수행하는 주체. 또는, 프로세스 내에 서 동작되는 어떤 실행의 흐름이다. 쓰레드가 등장하면서 쓰레드가 CPU 스케쥴링의 기본단위가 되었다.

process 내에 여러 thread가 있는 경우, 오른쪽과 같은 경우인데, code, data, files가 있는 메모리 영역은 모든 쓰레드가 공유하고, 각각의 thread는 독립적으로 실행되기 때문에 각각의 PC, register, stack(로컬 변수, 복귀 주소)을 보유하고 있는다

멀티쓰레딩의 Benefits

적은 비용으로 병렬처리가 가능하여 시간, 메모리 효율성이 높다. 함수 call을 사용해 쓰레드를 생성하기 때문에 코드의 구조가 단순화되어 readability가 좋다. 성능이 좋아지고 User-Interface와 Server 사이 반응속도가 높다. 또한, 자원의 공유가 쉽다.

Process vs Thread

쓰레드는 프로세스에 포함되는 개념이다. 프로세스 안에 여러개의 쓰레드가 존재할 수 있고, 한 프로세스 내의 쓰레드끼리는 같은 address space를 공유하기 때문에 쓰레드간의 데이터 공유가 쉽다.

  • 비슷한 점
    1. 각각의 control flow를 갖는다.
    2. 동시에 실행 가능하다. 프로세스는 CPU의 다른 코어에서 동시에 실행 가능하다.
    3. context switch가 일어난다.
  • 다른 점
    1. 한 프로세스 내 쓰레드끼리는 일부 자원을 공유한다. 하지만, 프로세스는 자원을 공유하지 않는다.
    2. 프로세스 컨트롤 비용이 쓰레드 컨트롤 비용보다 두배 정도 비싸다.
      Process
      Thread

pthreads

POSIX의 쓰레드를 생성하고 관리하는 라이브러리이다.

  • pthread_create : thread 생성
  • pthread_exit : thread 종료
  • pthread_join : 생성된 쓰레드가 종료될 때까지 wait

    위 예제에서 메인함수에서 pthread_create를 이용하여 threadfunc을 호출하여 쓰레드를 생성하면, threadfunc 함수에서 sleep(1)을 하는 동안 main이 출력되고 pthread_join이 호출되고 생성한 쓰레드가 종료될 때까지 기다린다. 그 사이에 Hello world!가 출력되고 return NULL을 통해 쓰레드가 종료되면 main2가 출력되고 메인함수가 끝난다.

동기화 문제


메인함수에서 20번 반복문을 돌면서 쓰레드를 생성하는데, 이때, count 변수를 넘겨줌으로써 20개의 쓰레드는 count를 동시에 접근한다. 쓰레드 20개 생성이 끝나면 아래에 있는 for문이 돌면서 각 쓰레드가 끝날 때까지 20개를 모두 기다린 후 count를 출력하고 끝나는 프로그램인데, 문제는 이 프로그램을 실행할 때마다 count 값이 엉뚱한 값이 나올 때가 있다는 것이다. 이것은, 20개의 쓰레드가 count를 동시에 접근하기 때문에 생기는 동기화문제이다.

Threading issues

  1. fork() / exec()
    멀티쓰레딩 환경에서 fork()를 할 때, 문제가 생길 수 있다. pthread에서는 fork()를 호출한 thread만 복사하여 자식 프로세스를 만들고, Unix에서는 부모프로세스의 모든 쓰레드를 복사하는 fork() 명령과 fork1()을 호출한 쓰레드만 복사하는 fork1()이 있다.
    만약, fork() 후 exec()를 호출하면 exec에 매개변수로 넘겨주는 프로그램 코드가 새로운 코드가 덮어씌워져 쓰레드를 복사하지 않아도 된다.
  2. Thread cancellation (쓰레드 취소(종료) 방법)
    Asynchronous cancellation(비동기 취소) : 쓰레드를 강제종료 시킨다. 만약 쓰레드가 어떠한 자원을 holding하고 있거나 공유되는 자원에 접근하여 값을 변경중이었다면? 문제가 생길 수 있다.
    Deferred cancellation(자연취소) : 해당 쓰레드에게 종료시킬 시점을 정해준다. 쓰레드는 주기적으로 종료되어야 하는지 확인한다. 해당 쓰레드가 만약에 중요한 작업을 하고있었다면, 그것을 처리하고 스스로 종료된다.
    *pthread는 두가지 모두 지원
  3. Signal handling
    일반적으로 멀티쓰레드 환경에서 signal을 별로 사용하지 않지만 어떤 프로세스에 signal이 전송되면, 프로세스 내에 어떤 스레드에 이 signal을 보내는 것이 맞는지에 대한 issue이다. 총 세가지 방법이 있는데, 첫번째로 모든 쓰레드에 해당 signal을 전달하는 방법이 있고, 두번째로 비트마스크를 이용해 쓰레드 당 처리하는 signal을 매핑시켜주는 방법이 있다. 세번째로, 어느 한 쓰레드에 모든 signal을 보내는 방법이 있다.
  4. 내부에 변수를 갖고 있는 라이브러리
    ex) errno는 어떤 동작에서 error를 파악하여 값을 세팅하는 변수인데, errno.h 라이브러리에 있다. 멀티쓰레드 환경에서 만약 쓰레드1에서 errno를 0으로 세팅하고 context switch가 일어나 쓰레드2에서 errno를 1로 세팅하고 errno를 확인하면 값이 1로 세팅되는 동기화 문제가 생긴다.
    결론은, 변수를 포함한 라이브러리는 해당 변수를 쓰레드끼리 공유가 가능해서 동기화문제가 생긴다는 것이다.

Multithread-safe

위의 thread-issue를 잘 생각하며 Multithread-safe하도록 thread를 생성하여야 한다.

kernel Thread vs user-level-thread

쓰레드를 적용하는 방법에는 두가지가 있는데, OS가 쓰레드를 관리하는 kernel thread, user-level(프로세스 내)에서 쓰레드를 관리하는 user-level-thread가 있다.
kernel thread는 pthread와 같이 시스템 콜을 이용해 운영체제에 요청하여 생성하고 관리한다. user-level-thread라이브러리가 쓰레드를 관리한다. user-level-thread가 가능한 이유는 쓰레드는 같은 address space를 공유하기 때문이다.

kernel thread

운영체제가 모든 thread 관련 작동을 관리한다. 그렇기 때문에 Thread Table이라는 것이 커널 내에 존재한다. *process는 원래 운영체제가 관리하기 때문에 process table이 커널 내에 있음.
단점
1. 그래도 좀 비싸다.
2. 모든 쓰레드 작동이 시스템 콜을 통해 운영체제가 관여하기 때문에 운영체제에 부담이 된다. 심지어 프로세스 내 쓰레드끼리 전환되는 경우에도 운영체제가 관여한다.
3. 운영체제가 각각의 쓰레드에 대한 상태값을 유지하고 있어야 한다.
** kernel-level-thread 관리는 process 관리와 비슷하다.

user-level-thread


더 싸고 빠르고 가벼운 쓰레드에 대한 고민의 결과로 user-level-thread가 등장하였다. 프로세스 내의 thread를 관리하는 라이브러리가 쓰레드를 관리한다. 프로세스 내에 Thread Table이 존재하기 때문에 커널이 아예 관여하지 않고, 커널은 user-level-thread의 존재 자체도 모른다. 각각의 쓰레드는 TCB(Thread Control Block)를 갖는다. kernel-thread에서는 시스템 콜로 쓰레드 관련 동작을 하였는데, user-level-thread에서는 함수 콜을 이용한다. kernel-thread보다 10-100배정도 빠르다.
단점
1. 운영체제가 관여하지 않기 때문에 운영체제와 통합되지 않아 바보같은 결정을 내릴 수 있다. 예를 들면 lock을 잡고 있는 thread를 스케쥴링하지 않는다던가, 현재 사용하지 않는데 스케쥴링 한다던가...
이를 해결하기 위해 kernel thread와 user-level-thread 사이에 협력이 필요하다.

user-level-thread의 context-switch

user-level-thread의 context switch는 라이브러리가 담당한다. 쓰레드의 스택에 현재 하드웨어 상태값을 저장하고 switch될 쓰레드의 하드웨어 상태값을 CPU에 세팅한다. 이 과정이 라이브러리 함수 콜을 통해 일어난다.

user-level-thread의 Scheduling

세 가지 큐를 통해 thread를 스케쥴링한다.
Run queue : 현재 실행중인 쓰레드
Ready queue : 실행되기 전 준비 상태인 쓰레드
Wait queue : 어떠한 이유로 block 된 쓰레드(I/O가 일어난다던가, lock이 걸려있다던가...)
만약, 한 쓰레드가 CPU를 독점하면 어떻게 되는가? user-level-thread는 OS가 관여하지 않아 Timer도 효과가 없는데... => Non-preemptive scheduling과 preemptive scheduling이 있다.

Non-preemptive scheduling

yield()를 호출하면 스케쥴러는 ready queue에 있는 thread를 cpu에 적재한다.
만약 쓰레드가 yield()를 호출하지 않는다면, 해당 쓰레드가 무한으로 CPU를 독점하게 된다.

preemptive scheduling

Non-preemptive scheduling이 이루어지지 않았을 때, 사용되는 방법으로, 스케쥴러는 OS에 timer interrupt를 요청하여 signal을 쓰레드에 보내어 강제로 CPU를 빼앗아온다.

Threading Model

  1. Many-to-One : 여러개의 user-thread를 하나의 kernel-thread에 매핑한다.
  2. One-to-One : user-thread의 갯수에 맞추어 kernel-thread를 생성하고 이를 CPU core에 적절히 매핑한다. => 대부분 운영체제가 이 방식을 사용한다.
  3. Many-to-Many : kernel-thread를 계속 만드는 것도 부담이니, kernel-thread를 적당히 만들고, user-thread를 매핑한다.

GO Lang

C++은 빠르다. 하지만 코드가 업데이트되고 복잡하고 빌드하는 시간이 오래 걸린다. 따라서 GO Lang이 등장했다. GO Lang은 쓰레드를 많이 만들어 병렬처리하는 시스템에서 사용된다. goroutine은 user-level-thread를 의미한다. GO Lang에서 쓰레드의 생성,삭제,context-switch가 가볍다.(user-level-thread를 다루기 때문에 OS를 거치지 않아서..) CPU Core당 하나의 Kernel thread만 만들어서 스케쥴링한다.

Go Lang의 컨셉
1. go thread를 idle-state로 두고 최대한 재사용하자.
2. runqueue에 접근하는 것을 제한하자.
3. runqueue를 분산시키자.

Ref. 충북대학교 조희승 교수 OS Course

profile
Record What I Learned

0개의 댓글