POSIX Threads

난1렙이요·2024년 12월 16일
0

시스템 프로그래밍

목록 보기
20/22

Motivation

  • 파일 디스크립터를 여러개 보고있는 프로그램을 만들고 싶다.
  • 만약 파일 디스크립터를 순차적으로 관찰하면 하나의 디스크립터를 관찰할 때 다른 것을 관찰하는 게 불가능하다.
  • 관찰에 많은 시간이 걸리게 되면 다른 파일 디스크립터를 불러오는 과정에 오차가 생긴다.
  • 이것을 해결하는 방법은 여러가지가 있다.
    • 변수를 공유하지 않는 여러개의 프로세스를 만든다.
    • select(), poll()같은 Blocking call을 이용한다.
    • Polling과 함께 Nonblocking I/O를 사용한다.
    • Asynchronous I/O를 사용한다.
    • Thread를 여러개 사용한다.
  • Thread를 여러개 만들어서 사용하면 독립적인 일들을 동시에 실행하면서 효율성을 늘릴 수 있다.
  • 언제 발생할 지 모르는 이벤트(Asynchronous Event)를 효율적으로 처리할 수 있다.
  • 병렬 수행을 통해서 높은 성능을 얻을 수 있다.

Thread

  • 각각의 Thread는 프로세스의 독립적인 실행 흐름을 나타낸다.
  • Thread마다 독립적인 실행 단위를 가지고 있다.
  • 각각의 Thread는 stack정보, CPU 상태를 별도로 가진다.(i.e. registers)
  • Thread들은 같은 코드, 전역 변수, heap공간 등을 공유한다.
  • 이러한 공유성 때문에 Multiple thread의 장점과 단점이 공존한다.
  • 그러므로, 2개의 프로세스가 통신하기 위해서는 OS를 사용하지만, 2개의 thread는 그냥 공유 memory를 통해서 통신할 수 있다.

Multitasking

  • Multitasking은 single processor에서도, multiprocessor에서도 할 수 있다.
  • Single Processor에서의 multitasking은 스레드들 사이를 사이를 번갈아가며 마치 동시에 수행하는 것처럼 보이게 한다.
  • Multi Processor에서의 multitasking은 실제로 스레드들이 동시에 실행된다.
  • 오늘날 OS는 멀티테스킹 시스템을 제공하며, 대부분 Multi Processor 환경에서 구현된다.

Processes vs. Thread

  • 프로세스의 특징들은 다음과 같다.
    • 프로세스는 독립적이다. 프로세스들은 공유하는 데이터가 없기 때문에 독립적이다.
    • 프로세스는 관리해야하는 상태 정보가 많다. 스레드는 실행 흐름이므로 프로세스의 부분이라고 할 수 있기 때문에 쓰레드보다 상태 정보가 많다.
    • 프로세스들은 분리된 메모리 주소를 가진다. 프로세스가 생성될 때 OS에 의해서 겹치지 않는 메모리 공간을 할당해주기 때문이다.
    • 프로세스는 별도의 방법을 통해 통신한다. 이는 OS의 도움이 필요하다는 의미이다.
    • 프로세스는 커널이 다룰 수 있는 "가장 무거운" 단위이다.
    • 프로세스가 생성될 때 OS는 필요한 것을 모두 만들어 준다.
      • 메모리 공간, 파일 핸들러, 소켓, 디바이스 핸들러, 윈도우 ...
    • 프로세스는 자신이 가진 것들을 다른 프로세스와 공유하지 않는다.
      • 예외로 파일 핸들러를 사용받거나 공유 메모리를 사용하면 공유할 수 있다.
  • 스레드의 특징들은 다음과 같다.
    • 프로세스 안에 존재하는 스레드들끼리 공유하는 정보들이 있을 수 있다.
    • 스레드들간에 스위칭이 일어나면서 훨씬 빠르게 동작할 수 있다.
    • 프로세스는 커널이 다룰 수 있는 "가장 가벼운" 단위이다.
    • 프로세스가 생성될 때 스레드는 적어도 하나 이상 만들어져야 한다.
    • 하나의 프로세스 안에 스레드가 여러개 있을 수 있다.(다중 스레드)
    • 스레드는 자체적으로 가지고 있는 리소스가 없다.
      • 예외로 스택, 레지스터 복사본, 프로그램 카운터... 등은 스레드가 자체적으로 가지고 있을 수 있다.
    • 스레드는 "kernel thread"와 "user thread"로 나뉜다.
      • kernel thread : OS가 직접 관리하며 커널에서 조작하는 스레드이다.
      • user thread : 커널은 스레드의 존재를 모르며 유저가 관리하는 스레드이다.

User Space

  • 위와 연결되는 내용으로 메모리 공간 또한 "kernel space"와 "user space"로 나뉜다.
    • OS는 가상 메모리를 사용하며, 이는 kernal space와 user space로 나뉜다.
    • 컴퓨터는 외부 저장 장치(Hard disk)에 있는 정보를 메모리가 읽으며 작동한다.
    • 그러나 외부 저장 장치에 있는 정보는 아주 많기 때문에, 필요한 정보만 메모리가 읽는다.(swap in)
    • 또한 필요 없는 정보는 외부 저장 장치에 다시 돌려보낸다.(swap out)
    • 그러나 swapping은 느린 과정이므로, 최소한으로 해야 한다.
  • kernel space : 커널이 실행할 때 사용하는 공간이기 때문이다. 대부분의 OS에서 kernel memory는 위에서 말한 swapped out되지 않는다.
  • user space : 유저 프로세스가 사용하는 메모리 공간을 말한다. 대부분의 swapping은 user space에서 일어난다.

Why Pthreads?

  • Posix thread, Pthread는 Posix에서 제공하는 thread 기능이다.
  • 왜 Pthread를 사용하는지는 아래 자료를 보면 알 수 있다.
    • 왼쪽은 fork를 사용한 것이다.(프로세스)
    • 오른쪽은 pthread_create를 사용한 것이다.(스레드)
    • 50,000개의 프로세스/스레드를 만드는 데 걸린 시간을 나타낸다.
  • 보이는 것 처럼, 스레드를 만드는 데 걸리는 시간이 훨씬 적다.
  • 이처럼 성능을 많이 챙길 수 있기 때문에 많이 쓰인다.

Pthread function

  • 스레드 관련 함수들의 종류이다.
    • pthread_cancel : 다른 스레드를 취소한다.
    • pthread_create : 스레드를 만든다.
    • pthread_detach : Set thread to release resources
    • pthread_equal : 두 스레드의 ID가 똑같은지 확인한다.
    • pthread_exit : 프로세스를 종료하지 않고 스레드를 종료한다.
    • pthread_kill : 스레드에 시그널을 보낸다.
    • pthread_join : 스레드를 기다린다.
    • pthread_self : 자신의 스레드의 ID를 알아낸다.
  • 대부분의 함수들은 성공하면 0을 return하며 오류가 나면 error 코드 값을 반환한다.
  • 이 함수들은 EINTR(인터럽트)신호를 보내지 않기 때문에 만약 발생하면 재시작할 필요가 없다.
  • thread함수는 pthread 라이브러리를 통해 사용한다. 그러므로 thread함수를 사용하려면 프로그램을 컴파일 할 때 pthread 라이브러리를 링크해줘야 한다.(-lpthread)

Self

#include <pthread.h>
pthread_t pthread_self(void);
  • pthrea_t pthread_self : 쓰레드의 ID값을 pthread_t값으로 반환한다.

Equal

#include <pthread.h>
pthread_t pthread_equal(thread_t t1, thread_t t2);
  • pthread_t pthread_equal : thread_t값 2개를 받아서 같은지 확인한다. 같으면 0이 봔환되며 다르면 0이 아닌 값이 반환된다.

Create

#include <pthread.h>
int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void *), void *restrict arg);
  • 스레드를 만든다.
  • 매개변수는 다음과 같다.
    • pthread_t *restrict thread : 새롭게 만들어진 스레드의 ID값이 여기로 반환된다.(Output parameter)
    • const pthread_attr_t *restrict attr : 스레드 또한 다양한 속성 값이 있다. 특정한 속성을 지정한 스레드를 만들고 싶을 때 사용하며, NULL값을 넣으면 기본 속성으로 생성된다.
    • void *(*start_routine)(void *) : 새로 만들어진 스레드가 동작할 함수의 이름을 넣어준다. 함수는 매개변수로 void* 를 가지며 반환도 void*를 한다.
    • void *restrict arg : 동작할 함수가 필요할 수 있는 매개변수를 설정한다.
  • 성공하면 0을 반환하며, 실패하면 0이 아닌 값을 반환한다
    • 여기서 반환하는 시기가 중요한데, 스레드가 끝나고 값을 반환하는 것이 아니라 언제 끝날지는 모르지만 생성했음을 알릴려고 반환하는 것이다.

Joinable과 Detach

  • 스레드는 처음 생성될 때 joinable한 상태이다.
    • joinable한 상태는 다른 스레드가 자신의 종료를 기다릴 수 있는 상태이다.
    • joinable한 스레드는 종료되면, 스레드가 사용한 값들을 바로 반환하지 않는다.
    • 왜냐하면 종료될 시 자신을 기다리고 있는 다른 스레드에게 값을 넘겨야 할 수 있기 때문이다.
  • Detach 함수를 통해서 스레드를 joinable → detach한 상태로 변화시킬 수 있다.
    • detach한 상태는 다른 스레드가 자신의 종료를 기다릴 수 없는 상태이다.
    • detach한 스레드는 종료되면, 스레드가 사용한 값들을 바로 시스템에 반환한다.
    • 이는 자신의 종료를 기다리는 스레드가 없으므로, 값을 남기지 않는다는 의미이다.

Detach

#include <pthread.h>
int pthread_detach(pthread_t thread);
  • 스레드를 Detach한 상태로 만들 수 있다.
  • pthread_t thread : detach 상태로 만들 스레드의 ID값이다.
  • 성공하면 0을 반환하며, 실패하면 0이 아닌 값을 반환한다.

Join

#include <pthread.h>
int pthread_join(pthread_t thread, void **value_ptr);
  • 어떤 스레드를 Joinable한 상태로 만들 수 있다.
  • 이는 다시말해, 어떤 스레드가 종료될 때 까지 자신은 기다린다는 의미이다.
  • 매개변수는 다음과 같다.
    • pthread_t thread : 기다릴 스레드의 ID값이다.
    • void **value_ptr : 종료되는 스레드가 기다리는 스레드에게 넘겨주는 값들을 받는 공간이다. 이중 포인터이므로 void* 값들을 받는 공간을 나타낸다.(Output parameter)
  • 성공하면 0을 반환하며, 실패하면 0이 아닌 값을 반환한다.
  • 대표적인 오류로 다음 함수가 있다.
    • pthread_join(pthread_self());
    • "나는 내가 종료될 때 까지 기다린다"는 코드이다.
    • 내가 기다리므로 종료하지 않기 때문에, 계속 기다리게 된다.
    • Deadlock이 걸린다.

Exit

#include <pthread.h>
void pthread_exit(void* value_ptr);
  • 프로세스를 종료하지 않으며 현재 스레드를 종료한다.
  • void* valut_ptr : 만약 자신을 기다리고 있는 thread가 있다면, 여기에 전달할 값을 넣어서 전달해줄 수 있다.
  • 스레드 안에서 return을 수행하는 것은 암묵적으로 pthread_exit를 수행하는 것과 동일하다.
  • 성공하면 0을 반환하며, 실패하면 0이 아닌 값을 반환한다.

Cancel

#include <pthread.h>
int pthread_cancel(pthread_t thread);
int pthread_setcancelstate(int state, int *oldstate);
  • 특정한 스레드가 동작을 중단하도록 하는 스레드이다.
  • 스레드는 취소 요청을 받을지, 받지 않을지 결정할 수 있다.
  • 만약 스레드가 취소 요청을 받지 않는데 취소 요청이 들어왔다면, 무시한다.
  • int pthread_cancel
    • 특정한 스레드가 동작을 중단하도록 한다.
    • pthread_t thread : 동작을 중단할 스레드의 ID값이다.
    • 스레드의 상태에 따라서 취소 요청을 받을지 안받을지 결정된다.
    • 성공하면 0을 반환하며, 실패하면 0이 아닌 값을 반환한다.
  • int pthread_setcanclestate
    • 현재 스레드의 취소 요청 상태를 설정한다.
    • 매개 변수는 다음과 같다.
      • int state : 취소 요청을 받을지, 받지 않을지 결정한다.
        • PTHREAD_CANCEL_ENABLE: 취소 요청을 받는다.
        • PTHREAD_CANCEL_DISABLE: 취소 요청을 받지 않는다.
      • int *oldstate : 기존 상태 값이 저장된다.
    • 성공하면 0을 반환하며, 실패하면 0이 아닌 값을 반환한다.

Cancel timing

#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);
void pthread_testcancel(void);
  • cancel 함수는 취소시키는 타이밍이 중요하다.
  • pthread_setcanceltype
    • 현재 스레드의 취소 시키는 타이밍을 지정한다.
    • PTHREAD_CANCEL_ASYNCHRONOUS : 취소 요청을 받는 즉시 취소한다.
    • PTHREAD_CANCEL_DEFERRED : 취소 요청을 받으면 지연시킨 후 내가 원하는 타이밍이 취소한다.
      • 내가 원하는 타이밍을 pthread_testcancle로 설정할 수 있다.
      • 이 함수가 불릴 때 만약 취소 요청을 받았었으면, 취소한다.

Thread safety

  • 멀티 스레드는 동시에 여러가지 작업을 처리할 수 있다.
  • 동시에 여러가지 작업을 처리하는 건 성능 면에서는 좋은 일이지만, 많은 오류가 생길 수 있다.
  • 예를 들어, 특정한 값을 변경할 때 두 곳에서 동시에 변경하려 하면 이전값이 반영이 되지 않는 등의 문제가 생길 수 있다.
  • 그러므로 스레드와 같이 함수를 사용하려면 그 함수가 스레드 환경에서 사용해도 문제가 없는지(safety) 알아보고 써야 한다.
profile
다크 모드의 노예

0개의 댓글