일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 다트 언어
- c언어
- c# 추상 클래스
- Flutter
- docker
- jupyter
- C++
- gitlab
- jupyter lab
- c# winform
- git
- c#
- 플러터
- C언어 포인터
- 구조체
- Python
- HTML
- Data Structure
- vim
- 깃
- C# delegate
- Houdini
- 유니티
- 도커
- Unity
- github
- Algorithm
- c# 윈폼
- dart 언어
- 포인터
- Today
- Total
nomad-programmer
[Programming/C] Mutex (상호배제, 상호배타) 본문
뮤텍스는 공유 데이터를 보호하는 락이다. 여러 스레드가 한 변수를 동시에 갱신하면, 결과는 예측할 수 없는 상태가 된다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
// 2백만개
int number = 2000000;
void error(char *msg) {
fprintf(stderr, "%s: %s", msg, strerror(errno));
exit(1);
}
void *decay_number(void *t) {
for (int i = 0; i < 100000; i++) {
number = number - 1;
}
return NULL;
}
int main(int argc, char *argv[]) {
void *result;
int ste;
pthread_t thread[20];
printf("스레드 연산 실행 전 number: %d\n", number);
for (int i = 0; i < (sizeof(thread) / sizeof(thread[0])); i++) {
ste = pthread_create(&thread[i], NULL, decay_number, NULL);
if(ste == -1){
error("스레드 생성 실패");
}
}
for (int i = 0; i < (sizeof(thread) / sizeof(thread[0])); i++) {
ste = pthread_join(thread[i], &result);
if(ste == -1){
error("스레드 종료 실패");
}
}
printf("스레드 연산 실행 후 number: %d\n", number);
return 0;
}
// 결과
/*
스레드 연산 실행 전 number: 2000000
스레드 연산 실행 후 number: 1827842
*/
결과값이 왜 예상치 못한 값이 되었을까? 모든 스레드가 다 실행된 후에 왜 number 변수는 0이 되지 않은 걸까?
:: 이유는 스레드가 서로 간섭을 일으켜 number 변수가 0이 되지 않았다. 그렇다면 왜 결과를 예측할 수 없을까? 그것은 스레드가 언제나 똑같은 시점에 실행되지 않기 때문이다. 어쩔 때는 서로 충돌을 일으키지 않고 어쩔 때는 충돌을 일으킨다.
스레드의 장점은 여러 수많은 일을 한꺼번에 처리하며 동일한 변수에 접근할 수 있다는 것이다.
단점은 모든 스레드가 동일한 변수에 한꺼번에 접근한다는 것이다.
멀티스레드 프로그램은 강력하지만, 필요한 곳에서 적절히 제어하지 않으면 예측할 수 없는 작동을 할 수 있다.
예를 들어, 자동차 두 대가 하나로 합쳐지는 길을 내려간다고 생각해보자. 사고를 막으려면 교통 신호등을 설치해야 한다.신호등이 있으면 차들이 동시에 공유된 자원(길)에 접근하지 않게 한다.
스레드 두 개 이상이 공유한 데이터 자원에 접근할 때도 마찬가지다. 두 스레드가 한 데이터에 동시에 접근하여 읽거나 쓰지 못하도록 신호등을 설치해야 한다.
두 스레드가 충돌하지 못하게 예방하는 신호등을 뮤텍스(Mutex)라고 부른다. 뮤텍스를 사용하면 아주 간단히 코드를 "스레드 안전" 상태로 만들 수 있다. (뮤텍스를 락(Lock)이라고도 부른다)
뮤텍스(MUTEX) = 상호(MUTually) + 배타(EXclusive)
뮤텍스를 신호등으로 사용하는 방법
스레드 간에 충돌할 수 있는 코드를 보호하려면 다음과 같이 뮤텍스를 만들어야 한다.
pthread_mutex_t a_lock = PTHREAD_MUTEX_INITIALIZER;
// PTHREAD_MUTEX_INITIALIZER 는 매크로이다. 전처리기가 이 매크로를 보면 뮤텍스를 만드는 코드로 치환해준다.
뮤텍스는 서로 충돌할 수 있는 모든 스레드가 볼 수 있어야 한다. 따라서 뮤텍스는 "전역 변수"로 만드는 게 일반적이다.
1. 빨간 불일 때 멈춘다.
중요한 코드가 시작되는 부분에 첫 번째 신호등을 설치해야 한다.
pthread_mutex_lock() 함수는 스레드 하나만 지나가게 만든다. 이 코드에 도착한 다른 스레드는 모두 기다려야 한다.
pthread_mutex_lock(&a_lock);
/* 중요 코드가 여기서 시작... */
2. 초록 불일 때 간다.
들어간 스레드가 중요 코드의 실행을 끝내면 pthread_mutex_unlock() 함수를 호출한다.
이 함수를 호출하면 신호등을 다시 초록색으로 만들어 기다리던 스레드 중 하나가 중요 코드에 들어갈 수 있게 된다.
/* ... 중요 코드 끝 */
pthread_mutex_unlock(&a_lock);
MUTEX Lock & Unlock 예제
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
// 2백만개
int number = 2000000;
// mutex lock
pthread_mutex_t decay_number_lock = PTHREAD_MUTEX_INITIALIZER;
void error(char *msg) {
fprintf(stderr, "%s: %s", msg, strerror(errno));
exit(1);
}
void *decay_number(void *t) {
// mutex lock
pthread_mutex_lock(&decay_number_lock);
for (int i = 0; i < 100000; i++) {
number = number - 1;
}
// mutex unlock
pthread_mutex_unlock(&decay_number_lock);
printf("number = %d\n", number);
return NULL;
}
int main(int argc, char *argv[]) {
void *result;
int ste;
pthread_t thread[20];
printf("스레드 연산 실행 전 number: %d\n", number);
for (int i = 0; i < (sizeof(thread) / sizeof(thread[0])); i++) {
ste = pthread_create(&thread[i], NULL, decay_number, NULL);
if(ste == -1){
error("스레드 생성 실패");
}
}
for (int i = 0; i < (sizeof(thread) / sizeof(thread[0])); i++) {
ste = pthread_join(thread[i], &result);
if(ste == -1){
error("스레드 종료 실패");
}
}
printf("스레드 연산 실행 후 number: %d\n", number);
return 0;
}
// 결과
/*
스레드 연산 실행 전 number: 2000000
number = 1900000
number = 1800000
number = 1700000
number = 1600000
number = 1500000
number = 1400000
number = 1300000
number = 1200000
number = 1100000
number = 1000000
number = 900000
number = 800000
number = 700000
number = 600000
number = 500000
number = 400000
number = 300000
number = 200000
number = 100000
number = 0
스레드 연산 실행 후 number: 0
*/
Q: 스레드를 사용하면 프로그램이 더 빨라지나?
A: 꼭 그런 건 아니다. 스레드를 사용하면 프로세스를 더욱 효율적으로 사용할 수 있지만, 락을 거는 방법에 주의해야 한다. 너무 자주 락을 걸면 단일 스레드 코드 만큼 느려질 수 있다.
Q: 어떻게 설계해야 스레드 코드가 빨라지나?
A: 스레드가 접근하는 데이터양을 가능한 한 줄이자. 스레드가 공유 데이터를 접근할 필요가 없으면 락을 걸 필요가 없고, 결국 코드가 훨씬 더 효율적으로 실행된다.
Q: 프로세스를 따로 만드는 것보다 스레드가 빠른가?
A: 일반적으로 스레드를 만드는 것보다 프로세스를 만드는 데 시간이 약간 더 걸리므로, 대체로 스레드가 더 빠르긴 하다.
데드락 (Dead Lock)
뮤텍스를 사용하면 '데드락'이 생길 수 있다.
가령 스레드가 두 개 있고 뮤텍스 A와 B가 있다고 치자. 이미 한 스레드가 A를 다른 스레드가 B를 갖고 있다.
B를 갖고 있는 스레드가 A를 가지려 하고,
A를 갖고 있는 스레드가 B를 가지려 하면 '데드락'이 발생한다.
두 스레드 다 원하는 뮤텍스를 가질 수 없기 때문에, 두 스레드 모두 더 이상 진행할 수 없는 것이다.
스레드 함수에 long형 값을 전달하는 방법
스레드 함수는 void 포인터형 인자 하나를 받고 void 포인터형 값을 반환할 수 있다.
종종 스레드에 정수형 값을 전달하고 정수형 값을 반환받아야 할 때가 있다. 이때 long형 값을 사용할 수 있다. long형과 void 포인터형의 크기가 같기 때문이다.
// 스레드 함수는 void 포인터 인자를 한 개 받을 수 있다.
void* do_stuff(void* param) {
// 원래대로 long형으로 변환
long thread_no = (long)param;
// 다시 void 포인터형으로 변환
return (void*)(thread_no + 1);
}
int main(){
pthread_t threads[3];
long t;
for(t = 0; t < 3; t++){
// long형인 t 변수를 void 포인터 형으로 변환
pthread_create(&threads[t], NULL, do_stuff, (void*)t);
}
void* result;
for(t = 0; t < 3; t++){
pthread_join(threads[t], &result);
// 반환된 값을 사용하기 전 다시 long형으로 변환
printf("스레드 %ld이(가) %ld를 반환하였다.\n", t, (long)result);
}
return 0;
}
// 결과
/*
스레드 0이(가) 1를 반환하였다.
스레드 1이(가) 2를 반환하였다.
스레드 2이(가) 3를 반환하였다.
*/
'Programming > C' 카테고리의 다른 글
[Programming/C] 자료형 범위의 값 (0) | 2020.06.21 |
---|---|
[Programming/C] static 키워드 (0) | 2020.06.21 |
[Programming/C] 스레드 (Thread) (0) | 2020.06.20 |
[Programming/C] 소켓과 서버 (0) | 2020.06.20 |
[Programming/C] 시그널 (Signal) (0) | 2020.06.20 |