[WEEK 07] 컴퓨터 시스템 - 12. 동시성 프로그래밍

신호정 벨로그·2021년 9월 25일
0

Today I Learned

목록 보기
35/89

논리 제어흐름은 이들이 시간적으로 중첩되면 동시적이며 이러한 일반적인 현상을 동시성(concurrency)이라고 한다.

응용 프로그램 수준의 동시성을 사용하는 응용 프로그램들은 동시성 프로그램이라고 한다.

12.1 프로세스를 사용한 동시성 프로그래밍

동시성 프로그램을 만드는 가장 간단한 방법은 프로세스와 fork, exec, waitpid와 같은 함수를 사용하는 것이다.

동시성 서버를 구현하는 자연스럽 방법은 부모에서 클라이언트 연결 요청을 수락하는 것이며, 그 후에 새로운 자식 프로세스를 생성해서 각각의 새로운 클라이언트를 서비스한다.

연결 요청을 수락한 후에 서버는 자식을 fork하고, 자식은 서버의 식별자 테이블 전체를 가져오게 된다.

1단계: 서버는 클라이언트로부터의 연결 요청을 수락한다.

2단계: 서버는 자식 프로세스를 fork하고 클라이언트를 서비스한다.

3단계: 서버는 다른 연결 요청을 수락한다.

4단계: 서버는 다른 자식을 fork해서 새 클라이언트를 서비스한다.

12.1.1 프로세스 기반 동시성 서버

첫째, 서버들은 대개 장시간 동안 돌아가므로 좀비 자식을 청소하는 SIGCHLD 핸들러를 포함해야 한다. SIGCHLD 시그널들은 SIGCHLD 시그널들은 SIGCHLD 핸들러가 돌고 있는 동안에는 블록되고, 리눅스 시그널들은 큐에 들어가지 않기 때문에 SIGCHLD 핸들러는 다수의 좀비 자식들을 청소할 준비를 해야 한다.

둘째, 부모와 자식은 자신의 connfd 사본을 닫아야 한다.

셋째, 소켓의 파일 테이블 엔트리 내의 참조 횟수 때문에 클라이언트로의 연결은 부모와 자식의 connfd 사본이 모두 닫힐 때까지는 종료되지 않을 것이다.

12.1.2 프로세서의 장단점

부모와 자식 프로세스 사이의 상태 정보 공유는 특정한 모델을 가지고 있다.

부모와 자식 프로세스는 파일 테이블을 공유하지만 사용자 주소공간은 공유하지 않는다.

한 개의 프로세스가 우연히 다른 프로세스의 가상 메모리를 쓰는 것은 불가능하다.

별도의 주소 공간은 프로세스가 상태 정보를 공유하는 것을 어렵게 한다.

12.2 I/O 다중화를 이용한 동시성 프로그래밍

12.3 쓰레드를 이용한 동시성 프로그래밍

동시성 논리흐름을 생성하는 두 가지 방법에는 프로세스와 I/O 다중화가 있다.

첫 번째 방법은 각각의 흐름에 대해 별도의 프로세스를 사용한다. 커널은 각각의 프로세스를 자동으로 스케줄링한다.

각 프로세스는 자신의 사적 주소공간을 가지므로 흐름들이 데이터를 공유하기가 어려워진다.

두 번째 방법은 자신의 논리흐름을 생성하고, 명시적으로 이 흐름들을 스케줄하기 위해서 I/O 다중화를 이용한다.

흐름들은 단 하나의 프로세스만 있기 때문에 전체 주소공간을 공유한다.

쓰레드를 이용한 세 번째 방법은 두 가지 방법을 혼합한 방식이다.

쓰레드

쓰레드프로세스의 컨텍스트 내에서 돌아가는 논리흐름이다.

쓰레드는 고유의 정수 쓰레드 ID(TID), 스택, 스택 포인터, 프로그램 카운터, 범용 레지스터, 조건 코드를 포함하는 자신만의 쓰레드 컨텍스트를 가진다.

한 개의 프로세스에서 돌고 있는 모든 쓰레드는 이 프로세스의 전체 가상주소를 공유한다.

(프로세스와 같이) 쓰레드는 커널에 의해 자동으로 스케줄되고, 커널에 정수 ID로 알려진다.

(I/O 다중화에 기초한 흐름에서와 같이) 다수의 쓰레드는 한 개의 프로세스의 컨텍스트에서 돌아가며, 프로세스 가상 주소공간의 전체 내용을 공유한다.

12.3.1 쓰레드 실행모델

각 프로세스는 메인 쓰레드라고 부르는 한 개의 쓰레드로 생명을 시작한다.

메인 쓰레드는 피어 쓰레드를 생성하고, 두 쓰레드가 동시에 돌아간다.

제어는 문맥 전환을 통해서 피어 쓰레드로 전달되며, 그 이유는 메인 쓰레드가 read나 sleep 같은 느린 시스템 콜을 실행하기 때문이거나 시스템 인터벌 타이머에 의해서 중단되었기 때문이다.

피어 쓰레드는 제어를 메인 쓰레드로 돌려주기 전에 잠시동안 실행하는 식으로 진행된다.

12.3.2 POSIX 쓰레드

POSIX 쓰레드(Pthreads)는 C 프로그램에서 쓰레드를 조작하는 표준 인터페이스다.

Pthreads는 데이터를 피어 쓰레드와 안전하게 공유하기 위해서 시스템의 상태 변화를 피어들에게 알리는 약 60개의 함수를 정의한다.

/* Pthreads "Hello, World" Program */
#include "csapp.h"

void *thread(void *vargp);

int main() {
    pthread_t tid;

    pthread_create(&tid, NULL, thread, NULL);
    pthread_join(tid, NULL);
    exit(0);
}

/* Thread routine */
void *thread(void *vargp) {
    printf("Hello, World!\n");

    return NULL;
}

12.3.3 쓰레드 생성

쓰레드는 pthread_create 함수를 호출해서 다른 쓰레드를 생성한다.

/* pthread_create 함수 */
#include <pthread.h>

typedef void *(func)(void *);

int pthread_create(pthread_t *tid, pthread_attr_t *attr, func *f, void *arg);

pthread_create 함수는 새 쓰레드를 만들고 쓰레드 루틴 arg새 쓰레드의 컨텍스트 내에서 입력 인자 attr를 가지고 실행된다.

pthread_create이 리턴할 때, 인자 tid는 새롭게 만들어진 쓰레드의 ID를 갖는다.

12.3.4 쓰레드 종료하기

쓰레드는 다음의 한 가지 방법으로 종료한다.

  1. 쓰레드는 자신의 최상위 쓰레드 루틴이 리턴할 때 묵시적으로 종료한다.
  2. 쓰레드는 pthread_exit 함수를 호출해서 명시적으로 종료한다. 만일 메인 쓰레드가 pthread_exit를 호출하면 다른 모든 쓰레드가 종료하기를 기다리고, 그 후에 메인 쓰레드와 전체 프로세스를 thread_return 리턴 값으로 종료한다.

12.3.5 종료한 쓰레드의 삭제 (Reaping)

쓰레드는 pthread_join 함수를 호출해서 다른 쓰레드가 종료하기를 기다린다.

pthread_join 함수는 쓰레드 tid가 종료할 때까지 멈춰 있으며, 쓰레드 루틴이 리턴한 기본 (void*) 포인터를 thread_return이 가리키는 위치로 할당하고, 그 후에 종료된 쓰레드가 가지고 있던 모든 메모리 자원을 삭제한다.

12.3.6 쓰레드 분리하기

연결 가능한 쓰레드는 다른 쓰레드에 의해 명시적으로 소거되거나, pthread_detach 함수를 호출해서 분리되어야 한다.

12.3.7 쓰레드 초기화

pthread_once 함수는 쓰레드 루틴에 관련된 상태를 초기화할 수 있도록 한다.

12.3.8 쓰레드에 기초한 동시성 서버

0개의 댓글