쓰레드 (Thread)

date
Apr 17, 2024
slug
thread
status
Published
tags
Computer Science
summary
type
Post
프로세스는 한 번에 하나의 일만 수행한다. 이때 제어 가닥을 여러 개 두어 하나의 프로세스에서 한 번에 여러 개의 일을 하도록 프로그램을 설계할 수 있다. → 각 쓰레드가 각자 개별적인 기능을 수행
단일 쓰레드에서 여러 기능을 처리할 때는 제어 가닥이 하나라서 암묵적으로 직렬화하여 처리할 수 밖에 없다. 그러나 제어 가닥이 여러 개일 때는 기능마다 개별적인 쓰레드를 배정하여 처리하면 된다. 이때 각각의 기능은 다른 기능 처리에 의존하지 않아야 한다. → 예: 메세지 프로그램에서 입력과 출력 처리를 쓰레드로 분리하여 동시성 해결
한 프로세스 안의 모든 쓰레드는 동일한 프로세스 구성요소들(파일 서술자(File Descriptor), 메모리 등)을 공유한다.
프로세스마다 Process ID (PID)가 있듯이 쓰레드에도 Thread ID (TID)가 있다. TID는 쓰레드가 속한 프로세스의 문맥 안에서만 의미가 있다.
쓰레드 ID의 출력이 유용한 경우는 별로 없다고 한다.
이 함수는 자신의 TID를 알아내는 기능이며, 자신의 TID를 알아내어 pthread_equal로 어떤 구조체가 자신을 위한 구조체인지 확인하는 용도로 사용된다.
notion image
예를 들어 감독 쓰레드가 새로운 작업들을 작업 대기열에 추가하며, 3개의 일꾼 쓰레드는 대기열에 있는 작업을 뽑아서 처리한다고 생각해보자.
이때 각 쓰레드에 알맞는 역할을 배정하고 싶다면, TID를 집어넣어서 쓰레드 입장에서 자신의 TID가 표시되어 있는 작업만 수행하게 하면 된다. - 쓰레드가 아무 작업이나 가져가지 않게 하는 방법
tidp가 가리키는 메모리 장소에 새로 생성된 쓰레드의 TID를 저장한다. 새로 생성된 쓰레드는 start_rtn 인자로 받은 함수의 주소에서 실행을 시작한다. 이때 실행되는 함수(start_rtn)에서 하나의 인자를 받을 수 있는데, arg 인자에 void 포인터로 전달할 수 있다.
만약 여러 개의 정보를 전달하고 싶다면 구조체에 저장한 후 구조체의 주소를 arg에 지정하면 된다.
쓰레드를 생성할 때 새 쓰레드가 먼저 실행될지, 함수를 호출한 쓰레드가 먼저 실행될지는 예측할 수 없다. - 쓰레드는 실행 순서가 중요하지 않고, 동시에 여러 작업을 수행하기 위함
쓰레드가 종료되는 경우
  • 쓰레드가 수행 중인 코드가 정상적으로 종료되어 반환한 경우
  • 다른 쓰레드가 현재 실행하고 있는 쓰레드를 종료하는 경우
  • 쓰레드가 pthread_exit를 호출하는 경우
pthread_join은 쓰레드의 종료를 대기하는 함수이다. 즉 쓰레드의 종료를 기다리되 쓰레드를 회수하지는 않는다.
쓰레드를 회수될 때 rval_ptr 인자의 반환 값으로 종료를 알릴 수 있다.
프로세스 기본 수단
쓰레드 기본 수단
설명
fork
pthread_create
새 제어 흐름을 생성한다.
exit
pthread_exit
기존의 제어 흐름에서 벗어난디.
waitpid
pthread_join
제어 흐름으로부터 종료 상태를 회수한다.
atexit
pthread_cleanup_push
제어 흐름에서 벗어날 때 호출될 함수를 등록한다.
getpid
pthread_self
제어 흐름의 식별자를 얻는다.
abort
pthread_cancel
제어 흐름의 비정상 종료를 요청한다.

쓰레드 동기화 (Thread Synchronization)

쓰레드는 여러 개의 제어 가닥을 만들어서 각각의 쓰레드가 개별적인 기능을 수행한다고 했다.
만약 어떤 쓰레드가 읽거나 수정하는 변수를 다른 쓰레드가 수정할 수 있다면, 접근할 때 유효하지 않은 값을 사용하지 않도록 쓰레드들을 동기화해야 한다. → 쓰레드가 변수를 수정하고 나서 다른 쓰레드들이 그 변수의 값을 읽을 때 일관성이 깨지게 된다.
이때 한 번에 하나의 쓰레드만 변수에 접근할 수 있게 하는 자물쇠(lock) 개념을 사용해야 한다.
notion image
예를 들어 변수의 값을 수정한다고 했을 때도 로우 레벨의 입장에서는 다음과 같은 단계를 거치게 된다.
  • 메모리 장소의 내용을 레지스터로 읽어 들인다.
  • 레지스터의 값을 증가한다.
  • 새 값을 메모리 장소에 기록한다.
만약 동기화 없이 두 쓰레드가 거의 동시에 같은 변수를 증가하려 하면 결과의 일관성이 깨질 수 있다.
notion image

뮤텍스 (Mutex)

뮤텍스는 상호 배제(mutual-exclusion)를 위한 방법으로 공유 자원에 접근하기 전에 잠그며, 공유 자원을 사용한 후에는 해제하는 방법이다.
뮤텍스가 풀리면 모든 쓰레드는 실행 가능 상태로 변하며, 그 중 가장 먼저 실행된 쓰레드가 뮤텍스를 설정하게 된다.
이러한 방법은 모든 쓰레드가 같은 접근 방식을 따르도록 해야 한다. 만약에 다른 쓰레드는 자물쇠를 획득하지도 않고 공유 자원에 접근한다면 다른 쓰레드는 자물쇠를 획득한 후 공유 자원에 접근한다는 규칙을 지켜도 비일관성이 발생할 수 있다.
뮤텍스를 초기화할 때는 PTHREAD_MUTEX_INITIALIZER 상수를 사용하거나 pthread_mutex_init 호출 시 뮤텍스 특성 구조체를 가리키는 인수에 널 포인터를 지정해주면 된다. → pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; - 정적(static)으로 할당된 뮤텍스 초기화
pthread_mutex_t lock; 선언 → pthread_mutex_init(&lock, NULL) - 동적(dynamic)으로 초기화
만약 뮤텍스를 동적으로 할당하는 경우 메모리를 해제하기 전에 반드시 pthread_mutex_destroy 함수를 호출해야 한다.
뮤텍스를 잠글 때는 pthread_mutex_lock, 뮤텍스를 풀 때는 pthread_mutex_unlock을 호출한다.
lock, unlock 개념을 사용한 왼쪽 예제는 tid[0] 쓰레드에서 Job 1을 출력할 때 잠금 상태가 되어 tid[1]은 접근하지 못하게 되며, tid[0]이 볼 일을 보고 나서 tid[1] 쓰레드가 동작하게 되어 순서대로 출력된다.
그러나 오른쪽 예제는 동시 다발적으로 동작하며, 쓰레드가 같은 변수(counter)를 사용하는 상황에서 lock, unlock 설정도 하지 않았기 때문에 출력 결과 또한 비정상적이다.

교착 상태 (deadlock)

한 쓰레드가 뮤텍스 1을 획득한 상태에서 뮤텍스 2를 잠그려 하고, 그와 동시에 다른 어떤 쓰레드가 뮤텍스 2를 획득한 상태에서 뮤텍스 1을 잠그려고 하면 어떻게 될까? → 두 쓰레드 모두 영원히 차단된다.
이러한 문제를 피하기 위해서는 뮤텍스가 잠기는 순서를 세심하게 제어해야 한다. → 모든 쓰레드가 항상 뮤텍스 B를 먼저 잠근 후에 A를 잠근다면 교착은 발생하지 않는다. - 잠그는 순서를 통일 → 교착의 가능성은 한 쓰레드가 뮤텍스들을 다른 쓰레드와는 반대의 순서로 잠그려 할 때에만 생긴다.

판독자-기록자 자물쇠 (reader-writer lock)

 

조건 변수 (condition variable)

 

회전 자물쇠 (spin lock)

 

장벽 (barrier)

 

참고자료


© hyuunnn 2024