728x90
반응형

process를 실행하기 위해서는 default로 하나의 thread가 생성이 된다. 

하드디스크 상의 program이 memory에 load되어서 process가 실행이 된다. process내에 thread가 실행이 되어서 실행되고 thread는 context 정보를 가지고 있다.

Thread가 여러 개인 process의 장점은 무엇일까? 다중 스레드가 좋은 점이 있다. 만약 task 하나를 수행하는 것이 하나의 함수이고 함수를 여러개 정의해 놓았는데 a()와 b()가 독립적인 함수다. 두 함수는 동시에 진행해도 상관이 없다고 하자. a()를 먼저 호출하고 b()호출할 수도 있지만 concurrent하게 실행할 수 있으면 cpu가 하나인 경우에도 process를 concurrent하게 실행할 수 있었다. (예를들어 fork()함수를 사용해서 하면 된다.)

a와 b라는 프로그램을 따로 만들어서 각각 실행시키면 두 프로세스가 concurrent하게 실행이 된다. 빠르게 switch해가면서 조금씩 실행해가면서 동시에 해가는 것처럼 보이는게 thread에도 적용이 된다. thread도 동시에 실행되면 서로 switch해가면서 동시에 진행된다. 다중 스레드는 concurrent한 task를 진행하기 위해서 사용한다. 

 

thread 개념과 특징

thread는 실행 단위를 나타낸다(실행 흐름) 실행 흐름이라는 것은 thread가 가진 중요한 정보 중 program counter라는 것이 있는데, thread마다 가지고 있는데 실행해야 되는 instructor에 다음 실행해야할 정보를 가지고 있는것이다. 하나의 실행흐름이 thread 객체이다. tread마다 독립적인 실행 단위를 가지고 있는 것이다. 각각의 tread는 cpu state register같은 독립적 정보를 가지고 있다. thread들 마다 따로 사용하는 메모리 공간이 있지만 thread들이 공유하는 메모리 공간이 존재한다. code 부분, 전역변수 부분, heap공간 들을 공유를 한다. (장,단점 존재)

다중 스레드라 하는 것은 다중 프로세스와 비슷한 성질이다. 그런데 여러 프로세스들은 프로세스들끼리는 memory를 공유하진 않는다. 프로세스간에 데이터 주고받는 것을 하고 싶으면 OS 도움 받아야함. 다중 스레드는 공유하는 메모리를 이용해서 thread들끼리 쉽게 데이터 주고받는 communication을 할 수 있다. 

스레드들은 독립적인 실행 단위. 사람눈으로 보기에는 동시에 실행되는것처럼 보인다. concurrent하게. 

 

다중 스레드 : 여러 독립적 task를 concurrent하게 실행. thread들간의 동기화 문제를 고려해야 한다.

1. 다중 스레드를 사용하게 되면 asynchronous events를 효율적으로 사용할 수 있다. (언제 발생할지 모르는 event)

2. parallel performance를 사용할 수 있다. 

 

Multitasking : 다중 프로세스, 다중 스레드로 가능하다.

single processor 에서는 time-division multiplexing. 실행시간 나눠서 task실행. 

multi processor 는 실제로 동시에 실행되는 것이다.   

 

Processes vs. threads

process들은 independent 하다. process가 사용하는 메모리 공간은 기존 process와 separate하게 os가 할당을 해준다. 한 프로세스 내에 있는 여러 개의 thread는 각자 사용하는 메모리 공간이 존재하고 공유하는 메모리도 존재한다. 서로 dependent한 부분이 있다. process는 thread에 비해 관리해야 하는 state information이 많다. context switch할 때 기존 상태를 저장하고 이전상태를 load해서 해야 하기 때문에 이 속도가 state양에 의해서 결정된다. 프로세스들은 separate한 address space를 가진다. process는 os의 도움을 받아야 process끼리 communication할 수 있다. (thread는 os도움없이 할 수 있다. 메모리를 공유하기 때문에)

 다중 프로세스보다는 다중 스레드로 하는 것이 overhead가 적게 든다. thread간의 동기화 문제를 고려해야 한다. process는 커널이 스케쥴링 할 수 있는 heaviest unit이다. 프로세스는 os가 할당해주는 자체적인 resource를 가지고 있다. process는 기본적으로 address space와 file resoure를 공유하지 않는다. (예외적인 경우로 file handle을 상속받거나 공유 segment를 share할 때 공유할 수 있는 메모리를 할당받을 수 있다. 이런 부분들은 default가 아니고 추가적인 작업이 있는 경우 할 수 있다. ) 프로세스들은 preemptive하게 multitasking된다. 어떤 프로세스가 cpu를 가지고 실행중이였다가 더 높은 우선순위가 ready queue에 들어오면 os는 preemptively한 것이면 우선순위 높은 process가 context switch가 일어나 선점될 수 있다.(언제든 실행중이였다가 scheduling 될 수 있다)

 

반면에 thread인 경우. process들끼리 공유하는 information이 있을 수 있다. (code, global, static, heap) thread들 간의 context switch의 overhead가 더 적다 -> switch시간이 더 빠르다. thread는 kernel이 스케쥴링하는 lightest한 unit이다. 한 프로세스 안에서 다중 스레드가 존재할 수 있다. Thread도 preemptive하게 해서 concurrent하게 진행할 수 있다. thread는 그 자체로 resource를 소유하지는 않는다. thread마다 독립적으로 소유하는 resource도 있다.(stack, a copy of the registers including the program counter, thread-local storage)

cf> thread는 kernel thread와 user level thread로 나누어볼 수 있는데 리눅스에서는 지금 구분하지 않는다. 

 

User space

: 사용자 application이 사용하는 메모리 공간. OS는 보통 kernel space와 user space로 나누어서 관리를 한다. kernel space는 kernel이 실행되는데 사용하는 공간, device driver에서 사용하는 공간이 있다. 대부분의 os에서 kernel memory는 disk에서 swapped out(교환)되지 않는다.

 프로그램이 하드디스크에 있으면 메모리에 로드한다. 가상메모리는 프로세스 실행할때 전체를 메모리에 로드하는게 아니라 memory가 하드디스크에 비해 작기 때문에 다 로드를 하는게 아니라 필요한 부분만 로드하고 필요없는 부분은 다시 하드디스크로 나가기도 한다. 이렇게 왔다갔다 하는것을 swapping한다라고 한다. 메모리가 로드되는것을 swap in 디스크로 나가는것을 swap out이라 한다. 하드디스크에 I/O 동작이 일어나는건데 메모리에 access하는것보다 하드디스크에 I/O 일어나는게 스와핑 과정이 아무래도 느리다. 그래서 시스템 입장에서 swapping하는것이 적은게 오버헤드가 줄어서 좋다. 커널 메모리에서는 swap out이 되지 않는다. user space는 swap in, swap out 과정이 일어난다. 

 

Pthreads

POSIX에 있는 thread library를 사용하면 된다. 줄여서 Pthread라고 얘기하고 POSIX 관련 시스템 콜 함수를 보면 함수의 이름도 다 pthread_ () 로 시작한다. 왜 pthread를 사용하느냐 -> 시간이 빠르다. 오버헤드가 더 적다. 

 

Thread management

- pthread_create : thread 생성

- pthread_join : thread 완료될때까지 기다림. 

 signal mask를 제어하기 위해서 sigprocmask() 함수 사용. 다중 스레드 프로세스에서는 사용하지 마라. -> pthread 함수중에 signal mask 제어하는 것을 사용해라. 

POSIX thread함수는 EINTR을 리턴하지는 않는다. -> interrupt되었을때 따로 재시작할 필요는 없다. 

 

pthread함수 사용

#include <pthread.h>

pthread library 사용해서 하면 -lpthread 해서 컴파일해줘야 한다. 

LIBTHREAD = -lpthread -> cc나 gcc사용해서 컴파일할때 libary 같이 컴파일해야된다는 점. 

 

Creating a thread

#include <pthread.h>
int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr,
                         void *(*start_routine)(void *), void *restrict arg);

pthread_create 함수 : thread 만들어서 시작까지 해줌(thread에게 할당된 task를 실행해달라를 OS에 요청)

파라미터의 의미

1: pthread_t* thread : output parameter. 생성된 thread의 아이디가 반환된다.

2: pthread_attr_t* attr : process와 마찬가지로 다양한 thread의 특징을 나타내는 속성정보. default가 아닌 지정한 속성을 가진 thread를 만들겠다고 할 때 사용.(default로 만들고 싶으면 NULL로) 

3: void*(*start_routine)(void*) : 함수 포인터. thread가 task하나를 담당한다. task는 함수로 정의한다. 새로 만들어진 thread가 기존 thread와 독립적으로 진행할 task를 설정한다. 아무 함수나 설정할 수 있는게 아니라 return 타입이 void*타입. 파라미터도 void* 타입. 

4: void* restrict arg : 3번째와 연관. 3번째 함수 실행할 때 필요할 수 있는 파라미터를 여기에 지정. 3번째 함수가 실행될 때 넘겨줄 input parameter. void*타입임. 파라미터 필요없으면 NULL로 설정. 

OS가 새로운 thread 생성하고 새로운 스레드로 정한 함수를 실행한다. 함수가 리턴이 되는 것은 스레드가 생성이 되고 지정된 함수의 동작을 실행하고 리턴하는게 아니고 0이 리턴이 되었으면 OS가 너의 요청을 성공적으로 받아들였다는 의미. 일단 요청을 성공적으로 받아들였다. task를 성공적으로 수행했다는 것은 함수를 수행했다해서 0을 리턴한게 아니고 지금 당장은 아니고 요청을 성공적으로 받아들였으니 OS가 thread 만들거다! 라는 것의 의미. 

 

Detaching

#include <pthread.h>
int pthread_detach(pthread_t thread);

thread는 detach <-> joinable.

detach한 스레드는 다른 스레드가 이 스레드의 종료를 기다릴 수 없는 스레드이다. detach상태 thread는 task완료해서 종료하면 스레드가 사용했었던 resource를 바로 release해버린다. detach스레드는 task를 완료한 다음에 resource를 바로 반환해 버린다.

 detach가 아닌 joinable인 원래의 스레드는 바로 release하지 않는다. 왜냐하면 joinable한 스레드는 다른 스레드가 이 스레드를 기다릴 수 있다. 스레드도 다른 스레드가 나를 기다리고 있다면 종료값을 넘겨줄 수있다. 반환해야 되는 리소스 중에 포함되어있다. 다 release해버리면 나의 종료값을 나의 스레드에게 넘겨줄 수 없다. 보통 스레드를 바로 반환하는게 아니라 유지를 하고 있다. 유지된 스레드에게 종료값을 넘겨줄 수 있다. 그런데 detach된 스레드는 exit하면 바로 반환해버린다. 이 함수는 internal oprtion을 스레드가 사용했던 리소스들을 exit할때 바로 reclaim할 수 있도록 만드는 함수다. detach함수는 종료 상태정보를 report하지 않는다. resource를 바로 반환하기 때문에. 

 

Joining

#include <pthread.h>
int pthread_join(pthread_t thread, void **value_ptr);

Join 함수 : join 함수를 호출한 thread는 첫번째 파라미터로 지정한 id의 thread가 종료될 때까지 기다렸다가 target thread가 종료하면 리턴하고 기다리는 thread에게 리턴값 넘겨줄 수있고 결과값을 2번째 파라미터로 받는다. void**. 이중 포인터의 의미는 리턴값을 받기 위해서 void 포인터이기 때문에 integer를 받기 위해서 integer 포인터를 선언하고 가리키는 integer의 메모리는 기다리는 대상이 만들어서 리턴을 해줄거라고 생각하고 포인터 변수의 위치를 넘겨준다. 포인터의 위치니까 포인터의 포인터가 된다. 포인터값이 스레드에 의해서 변경될 수도 있으니까 포인터의 위치를 넘겨준다. (포인터도 변경될 수 있기 때문에) 두번째 파라미터는 output parameter라는 것. 데이터를 communication할 수 있는것. 데이터의 전달이 일어나는 것이다.

 join 함수는 calling 함수를 suspend한다. nondetached thread의 리소스는 리소스를 release하지 않는다. 또는 전체 프로세스가 exit되기 전까지는 joinable한 리소스는 반환이 되지 않는다. 전체 프로세스가 exit되면 모든 리소스가 반환이 되는거기때문에 리소스가 반환이 된다. 

첫번째 파라미터는 target thread의 아이디, 두번째 파라미터는 포인터의 위치. 리턴값 받을거 없으면 NULL로 설정하면 된다. 마지막 pthread_self()를 join함수의 파라미터로 넣으면 안된다.  deadlock에 빠지게 되는 것이다. 이런 프로그램은 먹통이 되는것이다. 

 

Example of creation/joining

pthread를 하나 생성해서 task 끝낼때까지 기다렸다가 자식이 끝나면 나도 끝나는 간단한 프로그램. 

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void *processfdcancel(void *arg);

void monitorfdcancel(int fd[], int numfds) {
   int error;
   int i;
   pthread_t *tid;

   if ((tid = (pthread_t *)calloc(numfds, sizeof(pthread_t))) == NULL) {
      perror("Unable to allocate space for thread IDs");
      return;
   }
                         /* create a thread for each file descriptor */
   for (i = 0; i < numfds; i++)
      if (error = pthread_create((tid + i), NULL, processfdcancel, (fd + i)))
         fprintf(stderr, "Error creating thread %d: %s\n", i, strerror(error));
   for (i = 0; i < numfds; i++)
      if (error = pthread_join(*(tid + i), NULL))
         fprintf(stderr, "Error joining thread %d: %s\n", i, strerror(error));
   free(tid);
   return;
}

monitorfd -> file descriptor 여러개를 monitoring하는 것을 다중 스레드로 구현한 것. 모니터링할 fd는 array로 주고 array의 갯수를 numfds로 준다. calloc 함수로 fd의 개수만큼 thread를 만들어서 thread에게 각 스레드가 fd하나씩을 담당해서 읽어서 처리하는 함수를 실행을 시킬 것이다. id를 저장하기 위한 용도로 개수만큼 할당해서 저장. tid라는 포인터로 tid 포인터는 array가 되는 것이다. for문에 들어가서 갯수만큼 thread를 만든다. 원래 thread는 i 번째 스레드 종료하길 기다리고 종료하면 pthread 리턴하고 그 다음 스레드가 종료되길 기다렸다가 끝날때까지 기다렸다가 다 끝나고나면 mainthread도 종료. 여러개의 fd를 모니터링하는 함수 구현.

 

Exiting

#include <pthread.h>
void pthread_exit(void* value_ptr);

Exiting : pthread_exit함수는 스레드 종료할 때 호출하는 함수고 나를 기다리는 함수에게 넘겨줄 값을 void*를 넘겨서 리턴을 할 수 있다. 그냥 exit()는 process를 종료시키는 함수. return을 호출하면 pthread_exit함수해서 종료한다.

 

Cancellation

#include <pthread.h>
int pthread_cancel(pthread_t thread);
int pthread_setcancelstate(int state, int *oldstate);

이들 함수는 하나의 쓰레드에서 실행중인 다른 쓰레드를 종료하기 위한 목적으로 사용된다. 취소 요청을 받은 다른 쓰레드가 어떻게 작동할런지는 설정에 따른다. 요청을 받은 쓰레드는 바로 종료하거나 취소 지점을 벗어난 후 종료할 수 있다. 

pthread_cancel() 은 파라미터에 해당한는 쓰레드에게 종료 요청을 보낸다.

pthread_setcancelstate()는 호출한 쓰레드의 취소 상태를 변경하기 위해 사용된다. 상태는 (PTHREAD_CANCEL_ENABLE, PTHREAD_CANCEL_DISABLE) 중에 선택할 수 있다. 앞에꺼는 취소상태를 활성화 시키기 위해서, 두번째는 취소 상태를 비활성화 시키기 위해서 사용된다 

 

Example>

실제 thread를 생성해서 parameter 전달하고 return 값 받는 예제.

1. 별도의 thread가 file copy하는 작업을 함수를 써서 별도의 thread로 수행. copy 되는 동안 기다리는 것. copy된 총 바이트 수를 기다리는 thread에게 전달하는 프로그램. copy 될 함수의 source파일의 descriptor target file의 descriptor를 넘겨준다. array포인터를 넘겨준다. 

#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#define PERMS (S_IRUSR | S_IWUSR)
#define READ_FLAGS O_RDONLY
#define WRITE_FLAGS (O_WRONLY | O_CREAT | O_TRUNC)

void *copyfilemalloc(void *arg); //별도 스레드가 수행할 함수(형식 중요) 총 복사된 바이트 수 리턴

int main (int argc, char *argv[]) {        /* copy fromfile to tofile */
   int *bytesptr;
   int error;
   int fds[2];
   pthread_t tid;

   if (argc != 3) {
      fprintf(stderr, "Usage: %s fromfile tofile\n", argv[0]);
      return 1;
   }
   if (((fds[0] = open(argv[1], READ_FLAGS)) == -1) || //argv[1]이 소스파일 경로, argv[2] target파일 경로
       ((fds[1] = open(argv[2], WRITE_FLAGS, PERMS)) == -1)) {
      perror("Failed to open the files");
      return 1;
   }
   if (error = pthread_create(&tid, NULL, copyfilemalloc, fds)) { //thread생성하고 tid로 id저장
        fprintf(stderr, "Failed to create thread: %s\n", strerror(error));
      return 1;
   }
   if (error = pthread_join(tid, (void **)&bytesptr)) { //return넘겨줄때까지 pthread_join이 기다린다. heap 공간의 주소값이 bytesptr로 넘어온다. 
      fprintf(stderr, "Failed to join thread: %s\n", strerror(error));
      return 1;
   }
   printf("Number of bytes copied: %d\n", *bytesptr);
   return 0;
}

copyfilemalloc temp.c temp2.c

 

copyfilemalloc.c

#include <stdlib.h>
#include <unistd.h>
#include "restart.h"

void *copyfilemalloc(void *arg)  { /* copy infd to outfd with return value */
   int *bytesp;
   int infd;
   int outfd;

   infd = *((int *)(arg));
   outfd = *((int *)(arg) + 1);
   if ((bytesp = (int *)malloc(sizeof(int))) == NULL)
      return NULL;
   *bytesp = copyfile(infd, outfd);
   r_close(infd);
   r_close(outfd);
   return bytesp;
}

: 넘겨받은 argument가 int형의 array이다.

descriptor값은 소스파일 target 파일 담긴 시작 주소가 넘어온 것이다.

다음값을 포인터로 참조할려고 시작주소 +1 element 역참조니까 두번째 element target file descriptor 

heap 공간에다가 return값을 쓴다. 포인터값이 리턴이 된다. 

 

malloc을 사용하지 않고 return을 받을 방법이 없겠느냐-> stack을 사용해서 리턴받으면?

static int ret; ret = copyfile(); 받아서 return을 하기를. copy thread가 여러개면

malloc을 사용하지 않고 static정적 변수를 사용하면 copy하는 thread가 하나일떄만 작동. copy가 여러개면 복사결과값이 하나의 static 값에 덮어씌워지기 때문에 제대로된 결과값 얻을 수 없었다. 

thead에 파일 복사 할 방법이 뭐가 있겠느냐?

-> 이 예제에서만 통하는 방법이긴 한데 두개의 fd array를 넘겨준다. fd는 integer타입. return할때 integer포인터 타입으로 반환하긴했지만 총 바이트 수를 반환. 넘겨줄려했던 return타입이 같다. main thread가 공간을 만들고 thread로 넘겼었다. 방을 하나 더 만들자. array의 3번째 element에 복사한 결과물을 여기에 담아서 return을 하면 thread가 별도로 할당할 필요도 없고 static 할당할 필요도 없고 main thread에서 만들었던 공간을 access 할 수 있다. array에 담아버리면 별도로 return할 필요도 없다. 이 프로그램에서는 실제로 return도 한다. 세번째 array의 주소값을 return을 한다. 자식이 끝나고나서 복사된 바이트 수가 쓰여져 있는 것을 확인할 수 있다. 그런 아이디어를 이야기 하고 있는 것이다. thread는 malloc을 통해서 동적으로 할당한 메모리를 다 사용하고 난 다음에는 clean up해줘야 한다. 동적 메모리 할당한 것과 해제해야 하는것이 달라서 관리 측면에서 비효율적이다. main thread가 array에 pointer를 넘겨줄때 공간을 하나 더 추가해서 넘겨주어야 한다. array 크기가 2가 아니라 3으로 해서 넘겨줘야 한다. file 복사결과 총 바이트 수를 세번째 element에 copy하면 된다. return value는 두번째 파라미터나 만들었던 array의 3번째 element를 통해서 받을 수 있게끔 코드가 짜여져 있다. 

 

callcopypass.c

#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#define PERMS (S_IRUSR | S_IWUSR)
#define READ_FLAGS O_RDONLY
#define WRITE_FLAGS (O_WRONLY | O_CREAT | O_TRUNC)
void *copyfilepass(void *arg);

int main (int argc, char *argv[]) {
   int *bytesptr;
   int error;
   int targs[3]; //마지막은 return값을 받기 위한 방.
   pthread_t tid;

   if (argc != 3) {
      fprintf(stderr, "Usage: %s fromfile tofile\n", argv[0]);
      return 1;
   }

   if (((targs[0] = open(argv[1], READ_FLAGS)) == -1) ||
       ((targs[1] = open(argv[2], WRITE_FLAGS, PERMS)) == -1)) {
      perror("Failed to  open the files");
      return 1;
   }
      if (error = pthread_create(&tid, NULL, copyfilepass, targs)) { //함수를 복사하는 함수 넘겨주기
      fprintf(stderr, "Failed to create thread: %s\n", strerror(error));
      return 1;
   }
   if (error = pthread_join(tid, (void **)&bytesptr)) { //바이트 수를 저장하고 있는게 3번째 방이 넘겨지게 됨
      fprintf(stderr, "Failed to join thread: %s\n", strerror(error));
      return 1;
   }
   printf("Number of bytes copied: %d\n", *bytesptr); //복사된 바이트 수를 알 수 있다.
   printf("Number of bytes copied targs[2]: %d\n", targs[2]);
   return 0;
}
#include <unistd.h>
#include "restart.h"

// void* 타입의 array가 방 3개짜리 array가 넘어온것이다.
void *copyfilepass(void *arg)  {
   int *argint;

   argint = (int *)arg; //사실은 integer pointer
   argint[2] = copyfile(argint[0], argint[1]); // array index를 통해서 접근
   r_close(argint[0]);
   r_close(argint[1]);
   return argint + 2;
}
// 포인터 형식으로 참조할려면 *(((int*)arg)+2) 이렇게 쓰는거

malloc을 쓰지 않고도 thread의 결과값을 return 받을 수 있다. 

 

Wrong parameter passing

Thread를 생성하고 thread에게 parameter를 넘겨주는 예제인데 잘못된 프로그램이다. parameter를 잘못된 방식으로 passing하는 예제. void* printarg라는 함수를 별도의 Thread가 실행하는 task가 되는 것이다. printarg라는 함순데 이 함수의 동작은 fprintf함수를 써서 stderr장치 즉 화면에 Thread received하고 함수의 argument를 integer값을 출력. 함수의 argument는 사실 integer pointer로 넘어온다는 것이고 integer포인터가 가리키는 실제 integer값(역참조니까) 그러니까 main thread가 넘겨준 integer변수를 이 스레드가 화면에다가 출력하고자 하는 것이다. main에서는 10개의 thread를 만들어서 그 10개의 thread가 각각 printarg함수를 호출해서 화면에 자기가 넘겨받은 integer변수를 화면에 출력하도록 하는 것이다. 

pthread_t tid[NUMTHREADS]에 thread의 아이디가 저장된다. 그 다음에 for문을 돌면서 pthread_create함수를 돌면서 thread를 생성하고 생성된 i번째 생성된 thread의 아이디는 tid array의 i번째 element에 저장이 된다. 그리고 2번째 parameter는 NULL로 둔다. 새로 생성된 thread가 수행할 task는 printarg이다. (3번쨰 parameter) 넘겨주는 integer값이 i의 주소값. 여기서 i는 for문으로 돌리는 i이다. main thread가 for문의 각 iteration마다 i값을 넘겨준다. 첫번쨰 thread에게는 i가 0인값이 넘어가고 두번째는 i가 1인값.. 10번쨰 thread는 0부터 9까지의 숫자를 화면에 출력하고 끝난다. 순서는 thread가 어떤 순서로 출력할지 모르나 0부터 9까지가 화면에 나오기를 기대하고 프로그램을 짠 것이다. 결론적으로 이것은 잘못된 코드이다. 그다음에 for문은 10번 돌면서 pthread_join함수를 호출해서 기다린다. 종료가 되면 다시 for문을 돌아서 j가 1일떄 이렇게 해서 10개의 thread가 종료될때까지 기다렸다가 main thread가 all thread done하고 전체 프로그램이 종료된다. 

#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define NUMTHREADS 10

static void *printarg(void *arg) {
   fprintf(stderr, "Thread received %d\n", *(int *)arg);
   return NULL;
}

int main (void) {        /* program incorrectly passes parameters to threads */
   int error;
   int i;
   int j;
   pthread_t tid[NUMTHREADS];

   for (i = 0; i < NUMTHREADS; i++){
      if (error = pthread_create(tid + i, NULL, printarg, (void *)&i)) {
         fprintf(stderr, "Failed to create thread: %s\n", strerror(error));
         tid[i] = pthread_self();
      }
        }
   for (j = 0; j < NUMTHREADS; j++) {
      if (pthread_equal(pthread_self(), tid[j]))
         continue;
      if (error = pthread_join(tid[j], NULL))
         fprintf(stderr, "Failed to join thread: %s\n", strerror(error));
     }
   printf("All threads done\n");
   return 0;
}

0부터 9까지가 나왔어야 했었는데 실제 나온 결과는 0이 없다. 문제가 뭔가. 문제 상황이 일어날 수 있는 case는 처음에 main thread가 진행되면서 for문으로 들어와서 pthread_create함수를 호출한다. 그러면 os가 thread를 새로 생성하도록 요청한 것이다. 성공적으로 요청했으니까 return을 받는다. 그런데 이 첫번째 thread를 os가 만들어야 하는데 첫번째 thead가 만들어지는데 delay가 있는 것이다. 그 사이에 main thread는 for문을 다시 돌아서 2번째 pthread_create를 호출한다. 2번째 요청이 들어왔을때 thread가 생성이 되었다. 이 때 앞선 thread가 생성이 된것이다. arg값은 pointer가 가리키는 값이 main thread의 i값인데 지금 i값은 1이다. 그래서 첫번째 thread가 1을 출력하는 것이다. 

thread가 생성되서 parameter를 참조할 시점에 mainthread 때문에 참조할 것이 변경이 된 것이다. thread가 생성되는 타이밍에 따라서 mainthread의 i값이 출력이 되기 때문에. 

 

교제의 해결책 ---> 요청하는 타이밍을 늦추자. pthread_create할때마다 매 iteration마다 sleep을 둬서 1초씩 쉬었다가 다음 thread를 호출하자. 아무리 그래도 1초안에는 생성이 될것이기 때문에. i값을 받아서 잘 출력한다. 1초 이내에 thread는 만들어질것이다.

#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define NUMTHREADS 10

static void *printarg(void *arg) {
   fprintf(stderr, "Thread received %d\n", *(int *)arg);
   return NULL;
}

int main (void) {        /* program incorrectly passes parameters to threads */
   int error;
   int i;
   int j;
   pthread_t tid[NUMTHREADS];

   for (i = 0; i < NUMTHREADS; i++){
      if (error = pthread_create(tid + i, NULL, printarg, (void *)&i)) {
         fprintf(stderr, "Failed to create thread: %s\n", strerror(error));
         tid[i] = pthread_self();
      }
      sleep(1);
   }
   for (j = 0; j < NUMTHREADS; j++) {
      if (pthread_equal(pthread_self(), tid[j]))
         continue;
      if (error = pthread_join(tid[j], NULL))
         fprintf(stderr, "Failed to join thread: %s\n", strerror(error));
   }
   printf("All threads done\n");
   return 0;
}

완벽한 solution으로 볼 수 없는게 1초 이내에 thread가 생성되겠지라고 가정하고 있는 것이기 때문에 혹시나 thread생성하는데 1초이상 걸리는 경우가 생긴다면 이 solution은 같은 문제를 발생시킨다.

확실한 방법은 i를 절대 변경되지 않는 변수값을 넘겨주면 된다. 변수를 10개 만들어서 첫번째 thread에게는 첫번째 변수 두번째 thread에게는 두번째 변수. 아무리 thread가 생성된다 하더라도 변수값 parameter로 받아서 출력할 수 있다. 그러면 프로그램 로직이 변경되긴 해야 한다. 

 

Thread safety

내부적으로 static storage 사용하는 경우. 하나의 static storage에 여러 개의 thread가 같이 써서 충돌 문제가 생긴다. 그런 함수는 thread safe 한 함수가 아니다. thread safe한 함수는 여러 thread가 동시에 호출해도 task 수행하는데 문제가 없는 함수. safe하다는 관점에서는 async signal -> signal handler안에서 안전하게 호출할 수 있는. thread safe한 함수가 만드는게 쉬울까 async signal safe 한 함수 만드는게 쉬울까? async signal safe함수는 다중 thread가 아니더라도 충돌 문제가 생길 수 있다. 다중 스레드도 아니고 싱글 스레드에서도 생긴다. thread safe한 함수 만들기가 더 쉽다. POSIX에서는 thread safe 하도록 구현하도록 요구한다. (예전에 그랬다)

728x90
반응형

'CS > 시스템 프로그래밍' 카테고리의 다른 글

Signals  (0) 2021.12.07
Thread Synchronization  (0) 2021.12.01
Times and Timers  (0) 2021.11.16
UNIX Special Files  (0) 2021.11.03
Files and Directories  (0) 2021.10.15

+ Recent posts