컴퓨터/운영체제

[OS] Thread (3)

xeskin 2020. 4. 23. 13:19
반응형

이전에서 스레드 모델 이야기를 했다. 유저 레벨 스레드의 경우, 스레드를 제한없이 만들 수 있다는 등의 이야기를 했는데 스레드를 많이 사용하다보면 이를 생성/제거하는 것이 성능상의 오버헤드가 될 수 있다. 관리를 잘 못해주면 프로그램이 오작동할 수도 있다. 그래서 이를 잘 관리 해주기 위해 implicit threading을 해주는 툴들이 개발됐다. 이 툴들은 스레드 생성/관리를 런타임 라이브러리, 컴파일러를 통해 사용된다. 툴에는 Thread Pool과 OpenMP 두가지가 대표적이다.

 

Thread Pool

서버-클라이언트 시스템에서 스레드를 사용하는 모델이 있다고 생각해보자. 클라이언트가 서버에 리퀘스트를 보내면, 서버는 이를 실행시키기 위해 스레드를 생성하여 리퀘스트를 핸들링할 것이다. 그리고 작업이 다 끝났면, 스레드를 제거할텐데 이 과정이 반복되면 오버헤드가 생길 수 있다. 왜냐하면 스레드 생성에 상한이 없기 때문에 리퀘스트가 들어올 때마다 스레드를 생성/제거를 반복할 것이고 이는 시스템 리소스의 낭비를 가져온다. 더불어, 프로그래밍을 잘못 하여 스레드를 파괴하지 않고 생성만 하는 경우도 있을 수 있고,, 스레드 갯수가 많아질수록 context switching하는 것도 큰 오버헤드가 된다. 이때 문제점은 스레드 갯수에 상한이 없다는 것 때문에 생겼는데, 이를 해결하기 위해 스레드풀(Thread Pool)이 제안됐다.

 

스레드풀은 스레드를 미리 생성해서 풀에 담아두는 방법인데, 최대로 생성할 수 있는 스레드 갯수에 제약을 둔다. 그럼 하나의 리퀘스트가 들어왔을 때 미리 만들어둔 스레드를 할당하여 처리해준다. 처리를 다 하면 사용한 스레드를 풀에 반납하고, 이 스레드는 다른 리퀘스트가 들어올 때까지 풀에서 기다리고 있는다.

import java.util.concurrent.*;

Public class ThreadPoolExample
{
Public static void main(String[] args){
	int numTasks = Integer.parseInt(args[0].trim());
    
    /*Create the thread pool*/
    ExecutorService pool = Executors.newCachedThreadPool();
    
    /*Run each task using a thread in the pool*/
    for (int=0;i<numTasks;i++)
    	pool.execute(newTask());
        
    /*Shut down the pool once all threads have completed*/
    pool.shutdown();
}

자바에서는 다음과 같은 명령어로 스레드풀을 지원한다. 그리고 스레드풀 사이즈에 따라 지원하는 명령어가 다른데, 이는 다음과 같다.

/*creates a pool of size 1*/
newSingleThreadExecutor()

/*creates a thread pool with a specified number of threads*/
newFixedThreadPool(int size) 

/*creates an unbouned thread pool*/
newCachedThreadPool()

 

 

OpenMP

OpenMP는 C, C++, Fortran에서 지시어를 넣어주면 컴파일러가 멀티 스레딩을 할 수 있게끔 도와주는 set of compiler directives다. 프로코드를 바꿔 작성할 필요없이 지시어 몇 줄만 추가하면 멀티 스레딩을 할 수 있다. 공유 메모리(shared memory)환경에서. 많이 사용된다.

void simple(int n, float *a, float *b)
{
	int i;

	#pragma omp parallel for
	for (i=1; i<n; i++) /* i is private by default */
		b[i] = (a[i] + a[i-1]) / 2.0;
}

 

 

Threadin Issues

스레딩에 관한 몇가지 이슈를 알아보겠다.

- fork(), exec()

  하나의 스레드에서 fork()를 호출하는 경우에는 어떻게 될까? fork()는 자식 프로세스(childe process)를 생성하는 시스템 콜이다. 이는 부모 프로세스에 대한 모든 정보를 카피해온다. 그러면 이를 위해 메모리를 할당하는 등등 시스템 리소스를 사용할텐데, 곧바로 exec()를 사용하면 프로세스 전체가 지워진다. 그러면 리소스를 할당하는 것 자체가 불필요한 동작이 되는데, 유닉스에서는 이 문제점 때문에 fork()에 대한 두가지 옵션을 준다. 첫 번째는 모든 스레드를 카피하는 버전이고, 두번째는 fork()를 호출한 해당 스레드만 복사하는 버전이 있다.

 

- Signal Handling

  시그널은 유닉스 시스템에서 어떤 이벤트가 발생했다는 것을 프로세스에게 알릴 때 사용된다. 하나의 프로세스가 다른 프로세스에게 이벤트를, 하나의 커널이 다른 커널에게 이벤트를 전달하고 싶을 때 사용된다. 리눅스에서 cltr+c를 누르면 종료되는데, 이는 현재 실행되고 있는 프로세스에게 커널이 시그널이 보내 종료시키는 것으로 하나의 예시가 된다. 인터럽트가 발생하면 이를 처리하기 위해 인터럽트 핸들러가 사용되듯이 시그널을 처리하기 위해서는 시그널 핸들러가 사용된다. 이 시그널 핸들러는 자체적으로 제공되는 디폴트 핸들러와 유저 정의(user-defined)핸들러 두개로 나뉜다. 유저가 특정 시그널의 핸들러를 정의하지 않았을 때는 항상 디폴트 핸들러가 실행된다.

 

  이제 고려해봐야 할 점은 하나의 프로세스에 여러 개의 스레드가 있는 경우다. 우리가 시그널은 특정 스레드에만 전달해줘야 하는데, 스레드가 여러 개 있으면 시그널을 어떤 스레드에게 정해줄까? 우리에게는 네가지 옵션이 있다.

  1. 시그널이 적용되는 스레드에 전달한다.

  2. 프로세스의 모든 스레드에 전달한다.

  3. 프로세스의 특정 스레드에 전달한다.

  4. 프로세스에서 모든 시그널을 받을 특정 스레드를 정하여 그 스레드에게 전달한다.

  유닉스 시스템에서는 시그널을 전달하기 위해 kill이라는 함수를 사용한다. 

kill(pid_t pid, int signal)

pthread_kill(pthread_t tid, int signal)

  여기서 pid로 지정한 프로세스에게 특정 시그널을 보낼 수 있다. 주의해야 할 점은 프로세스에게 보내는 것이지, 스레드를 지정하지는 않는다. 그러면 특정 스레드에게 보내려면 어떻게 할 수 있을까? 이는 각 스레드별로 받들 시그널을 지정하면 해결할 수 있다. 하지만, 단점이 있는데 커널이 스레드에게 시그널을 보낸 뒤, 특정 스레드가 시그널을 받으면 다른 스레드는 그 시그널을 받을 수가 없다. pthread_kill은 이 단점을 보완했는데, 이를 사용하면 프로세스에서 시그널을 받을 특정 스레드를 지정해줄 수 있다.

 

- Thread Cancellation

  스레드 캔슬레이션은 스레드가 정상적으로 자신이 할 일을 다 하고 종료되는 것이 아닌, os의 커널이 미리 종료시키는 것을 말한다. 이때 스레드를 종료시키는 데는 두가지 옵션이 있다.

  1. 비동기적 캔슬레이션(Asynchronous Cancellation): 이는 없애고자 하는 타겟 스레드를 즉시 종료시키는 것을 말한다. 만약, 캔슬되는 스레드가 하나의 시스템 리소스를 사용하고 있는 도중에 캔슬되면 시스템 리소스가 붕 뜨게 된다. 즉, 리소스가 정상적으로 해제되지 않기 때문에 다른 스레드들이 그 리소스를 사용할 수 없는 문제점이 있다. 더불어, 스레드가 파일을 열어 업데이트를 하고 있는데 이를 도중에 캔슬하면 파일 내의 컨텐츠가 망가질 수 있다.

  2. 유예 캔슬레이션(Deferred Cancellation): 이는 캔슬할 스레드가 캔슬될 수 있을 때까지 기다린 뒤 캔슬하는 방법이다. 캔슬시킬 때 pthread_cancel()이라는 함수를 호출하는데, 캔슬하고자하는 타겟 스레드에게 캔슬 리퀘스트를 보낸다. 그런데 실제 캔슬 시점은 스레드의 모드와 상태에 달려있다. 스레드가 deferred mode에서 enabled state라면 기다렸다가 캔슬한텐데 이 시점은 어떻게 정해줄까? pthread.testcancel()을 중간에 넣어주면 cancellation point를 만들 수 있다. 실행되고 있는 스레드에 펜딩된 캔슬 리퀘스트가 온 것이 있는지 확인하고 캔슬하는 게 pthread.testcancle()이다. 그리고 스레드가 캔슬됐다면 클린업 핸들러가 호출되어 루틴을 실행한다.

pthread_t tid;

/*create the thread*/
pthread create(&tid, 0, worker, NULL)

/*cancle the thread*/
pthread_cancel(tid);

 

반응형

'컴퓨터 > 운영체제' 카테고리의 다른 글

[OS] Thread (2)  (0) 2020.04.16
[OS] Thread (1)  (0) 2020.04.16
[OS] Inter Process Communication (IPC)  (1) 2020.04.15
[OS] Context Switch와 레지스터 셋의 관계  (0) 2020.04.15
[OS] Interrupt  (0) 2020.04.03