Mutex란?

  • Mutual exclusion의 줄임말이다. lock을 건다는 말과 같다.
  • 스레드간에 동기화와 여러 스레드에서 같은 메모리에 쓸 때 공유되는 데이터를 보호한다.
  • mutex 변수는 공유 데이터를 접근하는 것을 보호하기 위해 lock처럼 사용된다.
  • mutex변수를 공유하는 스레드 중 하나의 스레드만이 mutex 변수에 lock을 걸 수 있다.
  • 공유된 변수를 번갈아가며 사용할 때 사용된다 (race condition 방지)

Mutex의 사용 예시

위 그림과 같이 은행 데이터베이스에 shyeon은 1000원, gildong은 1500원이 있다고 하자. 이때 A에서 shyeon에게 500원을 송금하고자하고 동시에 B에서 700원을 송금하고자 한다. 이때 무슨 일이 일어날까? 각각 A, B에서는 이런 일이 발생할 것이다.

  • 은행 DB로부터 shyeon의 계좌 정보를 변수에 저장한다.
  • 그 변수에 500원을 더한다.
  • 은행 DB에 계산 결과값을 저장한다.

A, B에서 위의 일이 동시에 발생한다면 A, B 모두 은행 계좌로부터 shyeon의 돈은 1000원이 있다고 읽어오고 A는 1000원에 500원을 더한 1500원을 은행 DB에 보낼 것이고 B는 700원을 더한 1700원을 DB에 보낼 것이다. 이때 더 늦게 도착하는 값으로 DB에 있는 shyeon의 잔고가 갱신될 것이다. 이런 상황을 race condition이라고 말한다. 어떤게 먼저 도착하냐에 따라 값이 달라지는 상황이다. 이런 일을 방지하려면 어떻게 해야할까? A나 B가 순서를 정하여 DB에 접근하면 된다. 이런 순서를 정하기 위해 mutex가 사용된다.

 

mutex 관련 함수

1. pthread_mutex_init

int pthread_mutex_init(ptrhead_mutex_t* mutex, const pthread_mutexatt_t *attr);

    mutex변수를 초기화하는데 사용하는 변수이다. mutex변수를 선언하여 첫 번째 인자에 주소값을 넘겨 초기화 할 수 있다. attr인자는 NULL로 하면 가본 속성으로 생성할 수 있다. 리눅스에서는 기본값인 NULL값을 사용한다. mutex변수를 초기화하는 또다른 방법으로는 다음과 같이 선언하면 된다.

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

    위의 코드처럼 mutex변수에 직접 대입하는 방법이다. 이 방법은 attr인자를 사용할 수 없다는 단점이 있지만 리눅스에서는 고려하지 않는다.

 

2. pthread_mutex_destroy

int pthread_mutex_destroy(pthread_mutex_t *mutex);

    사용이 끝난 mutex 변수를 해제하는 함수이다. malloc으로 동적으로 선언한 mutex 변수는 free()를 호출하기 전에 꼭 pthread_mutex_destroy()를 먼저 호출해야한다.

 

3. pthread_mutex_lock

int pthread_mutex_lock(pthread_mutex_t *mutex);

    mutex 변수에 lock을 거는 함수이다. 두 스레드의 함수에서 같은 mutex변수에 대해 각각 pthread_mutex_lock을 호출할 경우 두 스레드 중 하나의 스레드만 mutex 변수에 대해 lock을 얻고 다른 스레드에서는 pthread_mutex_lock에서 기다리고 있다. 이때 mutex를 얻은 스레드에서 pthread_mutex_unlock()을 호출할 경우 다른 스레드에서 mutex변수에 lock을 걸 수 있다.

 

예제

아래 코드는 두 스레드에서 공유하는 하나의 변수에 번갈아가며 출력하는 예제이다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int value;

void *func1(void *arg) { //1번 스레드가 실행할 함수
	for(int  i = 0; i < 5; i++) {
		pthread_mutex_lock(&mutex); //mutex 변수 lock(다른 변수에서 접근 불가능)
		printf("thread1 : %d\n", value); //변수 출력
		value++; //변수 증가
		pthread_mutex_unlock(&mutex); //mutex 변수 unlock(다른 변수에서도 접근 가능)
		sleep(1);
	}

	pthread_exit(NULL); //스레드 함수 종료
}

void *func2(void *arg) { //2번 스레드가 실행할 함수
	for(int i = 0; i < 5; i++) { 
		pthread_mutex_lock(&mutex); //mutex 변수 lock(다른 변수에서 접근 불가능)
		printf("thread2 : %d\n", value); //변수 출력
		value++; //변수 증가
		pthread_mutex_unlock(&mutex); //mutex 변수 unlock(다른 변수에서 접근 가능)
		sleep(1);
	}

	pthread_exit(NULL); //스레드 함수 종료
}

int main() {
	pthread_t tid1, tid2; //스레드 변수 선언

	value = 0; //공유 변수 초기화

	if(pthread_create(&tid1, NULL, func1, NULL) != 0) { //1번 스레드 생성
		fprintf(stderr, "pthread create error\n");
		exit(1);
	}

	if(pthread_create(&tid2, NULL, func2, NULL) != 0) { //2번 스레드 생성
		fprintf(stderr, "pthread create error\n");
		exit(1);
	}

	if(pthread_join(tid1, NULL) != 0) { //1번 스레드 종료 후 리소스 회수
		fprintf(stderr, "pthread join error\n");
		exit(1);
	}

	if(pthread_join(tid2, NULL) != 0) { //2번 스레드 종료 후 리소스 회수
		fprintf(stderr, "pthread join error\n");
		exit(1);
	}

	pthread_mutex_destroy(&mutex); //mutex 변수 해제

	exit(0);
}

 

실행 결과

 

mutex lock 실행 결과
mutex lock 주석처리 후 실행 결과

    첫 번째 사진은 mutex lock을 사용한 결과이고 두 번째 사진은 pthread_mutex_lock(), pthread_mutex_unlock()부분을 주석처리하고 실행한 결과이다. 실행 결과 1번 사진과 같이 1번 스레드와 2번 스레드가 번갈아가며 변수를 증가하며 출력하는 것을 볼 수있다. 2번 사진은 공유된 변수에 동시에 접근하여 같이 출력되는 것을 알 수 있다. 이렇게 여러 스레드가 공유 변수에 접근하고자할 때 race condition을 방지하기 위해 mutex를 사용할 수 있다.

 

    pthread 라이브러리를 사용할 때는 컴파일할 때 -lpthread를 포함해줘야한다. 그렇지 않으면 에러가 발생하고 컴파일이 되지 않는다.

 

개발 환경

wsl2 Ubuntu-20.04

gcc (Ubuntu 9.3.0-17ubuntu1~20.04)

스레드란?

  • 프로세스 내의 제어 흐름
  • 일반적으로 우리가 작성하는 코드는 단일 스레드 단일 프로세스
  • 다중 스레드 프로세스는 하나의 프로세스에 여러 컨트롤이 존재함

쉽게 말해 스레드란 우리가 프로그램을 실행할 때 코드가 실행되는 흐름이라고 할 수 있다.

 

특징

  • 동일 프로세스에서 동작하는 여러 개의 스레드는 코드 영역, 데이터 영역, 리소스(파일 디스크립터, 시그널 등)를 공유한다.
  • 스레드는 각각 Program Counter(PC)를 가지고 있다 - 이는 생각해보면 당연하다. 각 스레드는 실행하는 코드의 위치가 다르다. 즉 실행하는 명령어가 다르다. 따라서 각각 PC를 가지고 있어야한다.
  • 스레드는 스택 영역, 레지스터 집합, 스레드 ID를  각자 가지고 있다.

 

예시

그러하면 실제로 다중 스레드는 어디에 이용될까? 우리가 사용하는 웹 브라우저에서도 다중 스레드는 사용된다. 예를 들어 이미지, 텍스트 등 화면에 홈페이지를 표시해주는 스레드가 1개 존재한다면 백엔드로부터 데이터를 읽어오는 역할을 수행하는 스레드가 따로 동작하고 있을 수 있다. 이렇게 동시에 여러 기능을 수행하려면 다중 스레드를 이용하여야 한다.

 

함수

리눅스에서 제공하느 pthread 시스템 호출 함수를 이용하여 다중 스레드를 사용할 수 있다. 그럼 스레드를 제어할 수 있는 함수는 어떤 것들이 있는지 간단히 살펴보겠다.

  • pthread_create()     
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);

 - 새로운 스레드를 생성하는 함수이다. 함수의 원형은 위의 코드와 같다.

 - 첫 번째 인자로 새로 생성할 스레드의 ID가 저장될 주소 값을 넘겨준다.

 - 두 번째 인자는 기본 특성을 가지는 스레드의 경우 NULL을 넣어주고 pthread_attr_init()으로 pthread_attr_t 구조체를 초기화하여 사용자 모드 스레드나 커널 모드 스레드를 사용할 수 있다.

 - 세 번째 인자는 새로 생성되는 스레드가 실행할 함수의 포인터를 준다.

 - 네 번째 인자는 세 번째 인자의 함수에게 넘기고 싶은 정보를 구조체에 저졍하여 그 포인터를 넘겨주어 스레드가 실행하는 함수에서 그 정보를 사용할 수 있게 한다.

  • pthread_exit()
void pthread_exit(void *rval_ptr);

  - 스레드를 종료시키는 함수. 인자로 준 rval_ptr 값은 pthread_join의 두번 째 인자에서 받을 수 있다. 만약 pthread_detach()를 하지 않은 경우에는 스레드가 종료되었어도 스레드의 자원은 회수되지 않는다.

  • pthread_join()
int pthread_join(pthread_t thread, void **rval_ptr);

 - main스레드가 생성한 스레드가 종료될 때까지 기다리는 함수. 종료된 스레드의 자원을 회수하는 역할을 한다.

 - pthread_join()을 호출한 스레드는 그 스레드가 pthread_exit()을 호출할 때까지 대기한다.

 - main스레드의 종료로 인해 다른 스레드들이 강제로 종료되는 것을 방지한다.

 - 첫 번째 인자는 종료를 기다리는 스레드의 id이다.

 - 두 번째 인자는 pthread_exit()에서 넘겨주는 인자와 같은 값이다. 만약 스레드가 정상적으로 종료되었다면 두 번째 인자에 리턴 코드가 저장되고 스레드가 취소되면 rval_ptr이 가리키는 곳이 PTHREAD_CANCELED가 설정된다.

 - 만약 pthread_create의 두 번째 인자인 pthread_attr_t의 인자로 NULL을 준 경우는 스레드를 join해야한다.

  • pthread_detach() 
int pthread_detach(pthread_t tid);

 - pthread_join과 다르게 메인에서 스레드를 기다리지 않는 함수.

 - 스레드가 종료될 경우 자동으로 자원이 회수된다. (pthread_join을 할 필요 없다)

 - phtread_detach한 경우 pthread_join으로 스레드를 기다릴 수 없다. 즉 joinable하지 않다.

예제

실제로 다중 스레드를 이용하여 모두 공유하는 전역변수를 2개의 스레드가 1씩 증가시키며 출력하는 프로그램이다.

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

int var = 0;

void *func1(void *arg) { //1번 스레드 실행 함수
	while(var < 10) {
		printf("thread1 : %d\n", ++var);
		sleep(1);
	}
	pthread_exit(NULL); //1번 스레드 종료
}

void *func2(void *arg) { //2번 스레드 실행 함수
	while(var < 10) {
		printf("thread2 : %d\n", ++var);
		sleep(1);
	}
	pthread_exit(NULL); //2번 스레드 종료
}


int main() {
	pthread_t tid1, tid2;

	if(pthread_create(&tid1, NULL, func1, NULL) != 0) { //1번 thread 생성
		fprintf(stderr, "thread create error\n");
		exit(1);
	}

	if(pthread_create(&tid2, NULL, func2, NULL) != 0) { //2번 thread 생성
		fprintf(stderr, "thread create error\n");
		exit(1);
	}

	pthread_join(tid1, NULL); //1번 스레드 자원 회수
	pthread_join(tid2, NULL); //2번 스레드 자원 회수

	return 0;
}

 

결과

위 코드 실행 결과

위 그림처럼 두 개의 스레드가 각각 공유하는 전역 변수를 1씩 증가시키며 출력하는 것을 볼 수 있다. 하지만 이상한 점이 있다. 스레드가 실행되는 순서가 일정하지 않다는 것이다. 이렇게 스레드가 실행되는 순서는 생성되는 순서와 전혀 관계가 없다. 스레드의 실행 순서를 제어하려면 동기화가 필요하다. 동기화는 이후에 다뤄보겠다.

 

개발환경

OS : wsl2 Ubuntu-20.04

gcc : Ubuntu 9.3.0-17ubuntu1~20.04

+ Recent posts