mutex 코드가 무한 루프에 빠졌습니다.
#include <stdio.h> #include <pthread.h> #include <string.h> void* new_thread(void*); pthread_mutex_t mutex_1; char buf[1024]; int time_to_exit=0; int main(){ int res,i; pthread_t thread_1; void* thread_result; puts("making new thread"); pthread_mutex_init(&mutex_1,NULL); if((res=pthread_create(&thread_1,NULL,&new_thread,NULL))!=0){ perror("creating thread - failure"); exit(0); } pthread_mutex_lock(&mutex_1); puts("main locked"); printf("Input some thext. Enter 'end' to finish\n"); while(!time_to_exit){ fgets(buf,1024,stdin); pthread_mutex_unlock(&mutex_1); puts("main unlocked"); while(1){ pthread_mutex_lock(&mutex_1); puts("main locked"); if(buf[0]!=0){ pthread_mutex_unlock(&mutex_1); puts("main unlocked"); sleep(1); } else{ break; } } } pthread_mutex_unlock(&mutex_1); puts("waiting for thread joining"); if((res=pthread_join(thread_1,&thread_result))!=0){ perror("joining error"); exit(0); } puts("joining complete"); pthread_mutex_destroy(&mutex_1); exit(0); } void* new_thread(void* argv){ while(1){ pthread_mutex_lock(&mutex_1); puts("thread locked"); if(strncmp("end",buf,3)!=0){ printf("num of string is %d\n",strlen(buf)); sleep(1); buf[0]=0; pthread_mutex_unlock(&mutex_1); puts("thread unlocked"); } else{ time_to_exit=1; pthread_mutex_unlock(&mutex_1); break; } } pthread_exit(0); }
지금 주석처리를 해놓은 new_thread 의 unlock 이후 puts 를 주석처리하느냐 아니냐에 따라 무한루프에 빠지느냐 정상동작을 하느냐가 결정됩니다.
저한테는 마치 puts 의 수행시간이 없으면 new_thread 가 main 보다 먼저 lock 을 하는것처럼 보이는데,
그게 가능한 일인가요?
주석처리하면 이렇게 나오고
making new thread
Input some thext. Enter 'end' to finish
asdf
main locked
main unlocked
thread locked
num of string is 5
thread locked
num of string is 0
thread locked
num of string is 0
thread locked
num of string is 0
주석처리를 하지 않으면 정상동작을 보입니다.
making new thread
main locked
Input some thext. Enter 'end' to finish
129fdfsd
main unlocked
thread locked
num of string is 9
thread unlocked
main locked
main unlocked
thread locked
num of string is 1
thread unlocked
main locked
asdf
main unlocked
thread locked
num of string is 5
thread unlocked
main locked
sadfgfasdf
main unlocked
thread locked
num of string is 11
thread unlocked
main locked
혹시 조언 주시면 정말 감사하겠습니다.
?
풀 소스를 올려주세요.
전체 소스 올렸습니다.
전체 소스로 올렸습니다.
?
재미있네요. 제가 고수는 아니라서 보자마자 바로 아는건 힘들 것 같고
조금 더 분석해본 다음에 답변 드리겠습니다. (그 전에 다른 분이 해주시겠지만)
지금 생각으로는 userspace lock 이 원인인 것 같기는 합니다만(futex)
정확하지는 않기 때문에 다시 답변 드리겠습니다.
?
일단 100% 확실한 원인이라고 말씀드리긴 어렵지만, 대략적으로 이러합니다.
(아래 설명은 glibc/리눅스 버전 등에 따라 아주 약간의 차이가 있을 수 있습니다. 일단 x86 기준)
아시다시피 현재 glibc 는 pthread_mutex_lock / unlock 을 할 때에
내부적으로 futex syscall 을 사용하고 있는데, 이게 userspace lock 이다보니
넘겨주는 값(저기선 mutex_1)이 유저레벨에 있고 따라서 syscall 을 하기 전에
일단 먼저 해당 유저레벨의 값을 체크해서 락을 걸 수 있는 상태인 경우에
시스템 콜 호출 없이 바로 값을 변경하여 락을 걸어버립니다.
x86 기준으로 이는 물론 xchg 등의 atomic operation 을 이용해서 구현하게 되고
pthread_mutex_lock 코드(정확히는 어셈블리로 짜여있는 __lll_lock_wait) 에
이러한 코드가 반영이 되어 있습니다.
즉 락을 걸때는 시스템 콜의 호출 없이 유저 레벨에서 짧은 operation 으로 바로 걸 수 있기 때문에
위와 같은 차이가 날 수 밖에 없게 되는 것이 현재로서는 추측입니다.
좀 더 자세하게 동작을 설명하면 아래와 같습니다.
main thread 에서 먼저 입력을 받은 후 unlock 을 합니다.
그리고 puts("main unlocked") 를 하게 되는데, puts 는 굉장히
명령어를 많이 소모하는 함수이므로 이 과정 중간에 new_thread 에서 lock 을 걸고 들어갑니다.
(만약 저 main unlocked 를 출력하는 코드를 없애버리신다면 현재 일어나는 현상과 마찬가지로
main thread 에서 unlock 후에 바로 lock 을 다시 잡아버리는 현상을 보실 수 있습니다.)
그리고 new_thread 에서 이러저러한 처리 후에 unlock 을 하는데, 여기서 아시다시피
unlock 함수 호출 후에 다시 lock 함수를 호출하기까지 사이의 명령어는 단 2~3개 정도 뿐입니다.
그리고 아까도 말씀드렸다시피 futex 기반으로 구현된 락을 걸때는 미리 체크를 하고 들어가기 때문에
만약 아무도 락을 안 잡고 있다면 시스템 콜의 호출이 필요없이 빠르게 처리가 됩니다.
즉 new_thread 에서 unlock 을 할 때 내부적으로 futex_wake 시스템 콜(futex에 wait 옵션)을 호출하여
현재 이 락을 걸고 있는 쓰레드를 모두 깨우는데, 물론 메인 쓰레드도 여기에 포함되며 이 때 락을 잡다가
futex_wait 하고 있던 메인 쓰레드가 다시 깨어나서 동작을 시작하게 됩니다.
다만 언제까지나 futex_wait 시스템 콜 내부에서 멈춰있던 것이므로, 다시 유저 모드로 전환해서
__lll_lock_wait 에서 락을 잡을 수 있는지 확인하는 과정까지 모두 거치게 됩니다.
만약 위에서 얘기한 저 과정 사이에, new_thread 가 unlock 에서 futex_wake 를 호출하고
다시 유저 모드로 전환하여 락을 거는 과정이 더 빨리 끝날 경우 지금 보시는 현상이 나오게 됩니다.
일반적으로 제가 테스트해본 결과 futex_wait 하고 있던 쓰레드가 다시 유저 모드로 전환하여
락을 걸수 있는지 체크하는 코드까지 가는 시간이, futex_wake 호출하고 다시 나와서 락을 바로 거는
시간보다 약간 더 느린 것으로 보입니다. 물론 이것도 시스템마다 차이가 있으므로 항상 그런게 아닙니다.
결국 원인은 userspace lock 인 futex 는 아무도 락을 안 잡고 있을 때 락을 거는 코드가
유저 레벨에서 짧게 몇 개의 operation 으로 끝나기 때문에 락을 잡는 시간이 굉장히 짧다는 것,
그리고 futex_wake 호출 후에 다시 유저모드 전환하는 데에 드는 시간이 futex_wait 하고 있던
쓰레드가 wake 하고 다시 유저모드로 전환하는 데에 드는 시간보다 적기 때문입니다.
다만 이러한 테스트는 현재 시스템이 smp 인지 아닌지 등에 따라서도 언제든지 결과가 달라질 수 있는 내용입니다.
항상 그렇다는 것이 아니므로 이 점은 참조해주시면 될 것 같습니다.
그리고 위에서 시간차가 난다는 것도 항상 그렇단 얘기도 아니며, 굉장히 작은 차이로 추측되기 때문에
가령 예로 lock 후에 바로 unlock 하고 다시 lock 을 반복하는 코드를 메인 쓰레드에 만들어놓고
다른 쓰레드에서 lock 해놓고 대기해놓으면 위의 얘기대로라면 메인 쓰레드가 lock -> unlock -> lock -> ... 을 반복하며
락을 100% 점유하게 되어야 하지만 테스트 해보면 랜덤하게 수십번 정도 락을 잡았다 풀었다가 하다가 다른 쓰레드가
결국엔 락을 한 번은 잡게 됩니다. 즉 이 시간차도 유의미한 차이는 아니라는 얘기입니다.
위는 언제까지나 100% 확실한 내용이 아니고 직접 커널과 glibc 코드 뒤져보고 디버깅 해보면서 얻은 결론이므로
틀린 내용이 있으면 언제든지 지적 부탁드리겠습니다. 저도 개인적으로 조금 궁금하네요.
p.s 아 물론 저런 코드는 애초에 짜면 안됩니다. 저는 그냥 테스트용도로 짜신 거라고 생각했습니다만.
답변 감사합니다!
아직 공부가 부족해서 정확히 이해하지는 못하겠지만 뭘 몰랐었는지는 알겠습니다.
코드는 세마포어 포스트 웨이트 예제를 만들었다가 그대로 뮤텍스로 바꿔본거라 저모양입니다...;;
좋은 말씀 감사드립니다. 공부해야겠네요!
?
덧붙여서 물론 위 내용은 쓰레드 2개만을 기준으로 했을 때의 얘기입니다.
상당히 이상한 구조인데요
Mutex는 여러 프로세스가 동시에 락을 잡으려고 할 때 순서대로 공평하게 락을 나눠준다는 보장이 없습니다. 따라서 메인이 아닌 new_thread가 끝없이 계속 락을 잡는 건 정상적으로 일어날 수 있는 일입니다.
두 쓰레드가 서로 내용을 주고받고 해야 한다면 mutex 대신 pipe나 condition variable을 사용하시는 걸 권장합니다.
여기 비슷한 질문이 있네요:
http://stackoverflow.com/questions/8091117/why-sleep-after-acquiring-a-pthread-mutex-lock-will-block-the-whole-program
오 감사합니다!
링크주신 곳에 뮤텍스는 기본적으로 락을 잡고 있는시간보다 잡고있지 않는 시간이 길도록 코딩해야한다는 내용도 있군요.
생각해보면 효율상 당연한 이야기긴 한데,
FIFO 가 지켜지지 않는다는게 사실 어떠한 원리에서 가능한 일인지는 여전히 모르겠습니다.ㅠ
일단은 잘못을 알았네요!
감사합니다.
댓글 달기