Thread는 프로세스보다 가벼운 경량화된 프로세스라고 볼 수 있습니다. 스케줄러에 의해서 동작하는 가장 작은 processing 단위입니다. 하나의 프로세스 내부에서 여러 개의 Thread가 동작하는 것이죠.
정의는 알겠는데 그럼 Thread를 왜 사용하는거지? 그냥 process 쓰면 안되나? 라고 생각할 수 있습니다.
일단 기본적으로 사용하는 이유는 Process를 생성하는 것보다 시스템 자원 소모도 적고, 시간도 짧습니다. 그리고 Process-thread, thread-thread 사이에 자원 공유도 가능합니다.

그럼 간단하게 multi-thread를 하나 생성해보겠습니다.
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define NUM 5
void *print_msg(void *m);
int main(void)
{
pthread_t t1;
pthread_t t2;
pthread_create(&t1, NULL, print_msg, "hello");
pthread_create(&t2, NULL, print_msg, "world\n");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
}
void *print_msg(void *m)
{
char *cp = (char *)m;
for (int i = 0; i < NUM; i++)
{
printf("%s", cp);
fflush(stdout);
sleep(1);
}
return NULL;
}
thread를 사용하기 위해서는 pthread.h 라는 헤더 파일을 사용해야합니다.
thread 구조체를 하나 선언하고, pthread_create 함수를 통해 생성 후 정의된 함수를 실행합니다. 해당 코드에서는 pring_msg를 실행하겠네요.
pthread_join은 thread가 종료될 때까지 대기하며, 호출한 프로세스는 blocking 하는 함수입니다.
매크로로 NUM이 5로 선언되어 있으므로 print_msg 반복문은 5번 돌 것이고, 그러므로 hello와 world 2개가 5번씩 출력되고나면 thread가 종료될 것입니다.
가장 중요한 부분은 thread를 사용한 코드를 컴파일 할 때는 마지막에 꼭 -lpthread 옵션을 주어야합니다.

2가지 방식으로 실행을 해보았습니다.
첫번째는 실행 후 가만히 두었습니다. world를 출력할 때 마다 줄바꿈을 하면서 잘 출력되는 것을 볼 수 있습니다.
두번째는 동작 중에 SIGINT를 한 번 줘 보았습니다. 분명 process를 종료했는데 thread까지 같이 종료되는 것을 볼 수 있습니다. 이는 thread가 생성될 때 부모 프로세스의 메모리를 공유하기 때문에 프로세스 종료 시에 thread도 같이 종료되는 것입니다.
thread는 process 내부에서 동작하기 때문에, 전역 변수를 process와 공유합니다. 이는 보통 process와 thread간의 통신 용도로 사용됩니다. 동시에 메모리 접근을 허용하지만, 이로 인해 발생하는 문제가 있습니다.
예시 코드를 하나 보겠습니다.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <ctype.h>
void *count_words(void *f);
int total_words = 0;
int main(int ac, char *av[])
{
pthread_t t1, t2;
if (ac != 3)
{
printf("usage: ./twordcount1 file1 file2\n");
exit(1);
}
pthread_create(&t1, NULL, count_words, (void *)av[1]);
pthread_create(&t2, NULL, count_words, (void *)av[2]);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("%d: total words\n", total_words);
return 0;
}
void *count_words(void *f)
{
char *file_name = (char *)f;
FILE *fp = fopen(file_name, "r");
int c, prevc = '\0';
if (fp == NULL)
perror(file_name);
while ((c = getc(fp)) != EOF)
{
if (!isalnum(c) && isalnum(prevc))
{
total_words += 1;
}
prevc = c;
}
fclose(fp);
return NULL;
}
위 코드는 total_words라는 하나의 전역 변수에 두 개의 thread가 접근하는 예시입니다.
텍스트 파일에서 단어를 읽고, total_words에 개수를 하나 추가하여 두 개의 파일에 총 몇 개의 단어가 있는지 출력합니다.
순서대로 하나 하나 추가하면 이상적이겠지만, 동시에 같은 수에 +1을 하려하면 문제가 발생합니다. 만약 total_words가 10이고, 두 thread가 동시에 10에다 1을 더하는 작업을 하면, 둘 다 total_words = 11 이라는 작업을 수행하기 때문에, 단어 하나만큼의 오류가 나게 됩니다.

1번째 경우에서는 2번, 마지막엔 3번 동시 접근이 일어났다고 볼 수 있겠네요.
이러한 문제를 해결하기 위해서 Critical Section(임계 영역)이라는 것을 지정합니다.
이는 아까 위의 예시에서 봤듯이, 공유 자원의 접근 순서에 따라 실행 결과가 달라지는 영역으로, 공유 자원의 독점권을 보장해야 되는 영역입니다.
Mutex는 Mutual Exclusion의 약자로, 동시에 여러 thread가 공유 자원에 접근하는 것을 허용하지 않는 동기화 기법입니다. lock을 얻은 thread만이 접근이 가능합니다.
칸이 하나인 공중 화장실을 생각하시면 편하겠네요. 안에 들어가서 문을 잠구고 사용하는 방식이라고 생각하는 거죠. 그리고 사용 후엔 lock을 반납합니다.
위의 예시에서 코드를 조금 추가한 예시를 보겠습니다.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <ctype.h>
void *count_words(void *f);
int total_words = 0;
pthread_mutex_t counter_lock = PTHREAD_MUTEX_INITIALIZER; // 이 매크로 사용 시 pthread_mutex_init() 호출 필요 없음
int main(int ac, char *av[])
{
pthread_t t1, t2;
if (ac != 3)
{
printf("usage: ./twordcount1 file1 file2\n");
exit(1);
}
pthread_create(&t1, NULL, count_words, (void *)av[1]);
pthread_create(&t2, NULL, count_words, (void *)av[2]);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("%d: total words\n", total_words);
return 0;
}
void *count_words(void *f)
{
char *file_name = (char *)f;
FILE *fp = fopen(file_name, "r");
int c, prevc = '\0';
if (fp == NULL)
perror(file_name);
while ((c = getc(fp)) != EOF)
{
if (!isalnum(c) && isalnum(prevc))
{
pthread_mutex_lock(&counter_lock); // lock
total_words += 1;
pthread_mutex_unlock(&counter_lock); //unlock
}
prevc = c;
}
fclose(fp);
return NULL;
}
이렇게 함으로써 더 이상 서로 다른 thread가 하나의 공유 자원에 동시 접근하는 것이 불가능해졌습니다.

하지만 mutex에도 단점은 있습니다. 프로그램의 속도가 저하된다는 것이죠. 위의 예시는 체감이 되지 않겠지만, 처리하는 양이 많아질수록 느려짐이 체감될 것입니다. 이를 해결하기 위해서는 각각의 thread가 자신의 counter 변수를 사용하는 것이 효율적입니다.
또 조금 더 변화된 예시를 하나보겠습니다.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <ctype.h>
struct arg_set
{
char *file_name;
int count;
};
void *count_words(void *f);
int main(int ac, char *av[])
{
pthread_t t1, t2;
struct arg_set arg1, arg2;
if (ac != 3)
{
printf("usage: ./twordcount1 file1 file2\n");
exit(1);
}
arg1.file_name = av[1];
arg1.count = 0;
pthread_create(&t1, NULL, count_words, (void *)&arg1); // 구조체 주소를 넘김
arg2.file_name = av[2];
arg2.count = 0;
pthread_create(&t2, NULL, count_words, (void *)&arg2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("%d: %s\n", arg1.count, arg1.file_name);
printf("%d: %s\n", arg2.count, arg2.file_name);
printf("%d: total words\n", arg1.count + arg2.count);
return 0;
}
void *count_words(void *arg)
{
struct arg_set *arg_p = (struct arg_set *)arg;
FILE *fp = fopen(arg_p->file_name, "r");
int c, prevc = '\0';
if (fp == NULL)
perror(arg_p->file_name);
while ((c = getc(fp)) != EOF)
{
if (!isalnum(c) && isalnum(prevc))
{
arg_p->count += 1;
}
prevc = c;
}
fclose(fp);
return NULL;
}
전역 변수 사용을 막기 위해 파일명과 count가 들어가는 구조체를 하나 만들고, 각각의 thread에 각각의 구조체를 사용하는 형식을 사용했습니다. 이런 방식을 사용하면 같은 자원에 접근할 일도 없고, 작업을 병렬적으로 처리하기 때문에 더 빠른 속도로 처리할 수 있습니다.

Thread 간 메시지를 전달할 수 있는 방법이 있습니다. 문자 메시지 같은 건 아니구요,,, 하나의 thread가 작업을 일찍 마친 경우 pthread_cond_wait로 대기하고 있는 thread에 pthread_cond_signal을 통해 전달할 수 있습니다.
예시를 보면 쉽게 이해할 수 있습니다.
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
int data = 0;
void *print_data(void *arg)
{
while (1)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex); // 대기
printf("print_data() data: %d\n", data); // 출력
pthread_mutex_unlock(&mutex);
}
}
void *inc_data(void *arg)
{
while (1)
{
pthread_mutex_lock(&mutex);
data++; // 증가
pthread_cond_signal(&cond); // signal 전송
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
int main()
{
pthread_t thread1, thread2;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
pthread_create(&thread1, NULL, print_data, NULL);
pthread_create(&thread2, NULL, inc_data, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_mutex_destroy(&mutex); // mutex 변수 해제
pthread_cond_destroy(&cond); // cond 변수 해제
return 0;
}
cond 구조체를 선언하고, 이를 통해 신호를 주고 받습니다.
print_data thread에서는 wait를 통해 기다리고, inc_data에서는 signal을 통해 신호를 보냅니다.
물론 mutex를 사용하여 동시 접근도 막습니다.

이렇게 매우 간단한 예시를 살펴봤습니다. 이 개념을 이용해서 프로그램을 하나 만들어봅시다.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <ctype.h>
struct arg_set
{
char *filename;
int count;
};
struct arg_set *mail_box;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 초기화
pthread_cond_t flag = PTHREAD_COND_INITIALIZER; // 초기화
void *count_words(void *);
int main(int ac, char *av[])
{
pthread_t t1, t2;
struct arg_set arg1, arg2;
int reports_in = 0;
int total_words = 0;
if (ac != 3)
{
printf("usage: ./twordcount4 file1 file2\n");
exit(1);
}
arg1.filename = av[1];
arg1.count = 0;
pthread_create(&t1, NULL, count_words, (void *)&arg1);
arg2.filename = av[2];
arg2.count = 0;
pthread_create(&t2, NULL, count_words, (void *)&arg2);
while (reports_in < 2)
{
pthread_mutex_lock(&lock);
printf("MAIN: waiting for flag to go up\n");
pthread_cond_wait(&flag, &lock);
printf("MAIN: wow! flag was raised, I have the lock\n");
printf("%d: %s\n", mail_box->count, mail_box->filename); // 단어 개수 출력
total_words += mail_box->count;
if (mail_box == &arg1)
{
pthread_join(t1, NULL);
}
if (mail_box == &arg2)
{
pthread_join(t2, NULL);
}
mail_box = NULL;
pthread_cond_signal(&flag);
reports_in += 1;
pthread_mutex_unlock(&lock);
}
printf("%d: total words\n", total_words);
return 0;
}
void *count_words(void *arg)
{
struct arg_set *a = (struct arg_set *)arg;
FILE *fp = NULL;
int c, prevc = '\0';
if ((fp = fopen(a->filename, "r")) != NULL)
{
while ((c = getc(fp)) != EOF)
{
if (!isalnum(c) && isalnum(prevc))
{
a->count += 1;
}
prevc = c;
}
fclose(fp);
}
else
perror(a->filename);
printf("---------------------------------------\n");
printf("COUNT: waiting to get lock(id: %lu)\n", pthread_self());
pthread_mutex_lock(&lock);
printf("COUNT: have lock, storing data(id: %lu, file: %s)\n", pthread_self(), a->filename);
if (mail_box != NULL)
{
pthread_cond_wait(&flag, &lock);
}
mail_box = a;
printf("COUNT: raising flag\n");
pthread_cond_signal(&flag);
printf("COUNT: unlocking box(id: %lu)\n", pthread_self());
pthread_mutex_unlock(&lock);
printf("---------------------------------------\n");
return NULL;
}
쉽게 설명하자면 하나의 우편함을 두고, 여러명의 사람이 이용하는 것입니다. 그런데 우체통은 열려있을 때 한 번에 한 사람만 사용할 수 있고, 그 우체통에 파일의 단어 수를 적어 넣습니다. 이후 우체부가 수거 후 그 결과를 기록하고 남은 사람에게 사용하라고 알려줍니다. 이 과정을 thread의 수(=파일의 수)만큼 반복합니다.

thread 하나가 동작하는 동안 main 함수는 대기하고, 그 thread가 신호를 보내면 결과를 출력 후 tread를 종료합니다. 그리고 남은 하나의 thread가 동작하는 동안 main은 또 대기하고 신호를 보내면 결과를 출력하면 프로그램은 종료됩니다.