mutex 코드가 무한 루프에 빠졌습니다.

leadha의 이미지

#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

혹시 조언 주시면 정말 감사하겠습니다.

익명 사용자의 이미지

풀 소스를 올려주세요.

leadha의 이미지

전체 소스로 올렸습니다.

익명 사용자의 이미지

재미있네요. 제가 고수는 아니라서 보자마자 바로 아는건 힘들 것 같고
조금 더 분석해본 다음에 답변 드리겠습니다. (그 전에 다른 분이 해주시겠지만)

지금 생각으로는 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 아 물론 저런 코드는 애초에 짜면 안됩니다. 저는 그냥 테스트용도로 짜신 거라고 생각했습니다만.

leadha의 이미지

아직 공부가 부족해서 정확히 이해하지는 못하겠지만 뭘 몰랐었는지는 알겠습니다.

코드는 세마포어 포스트 웨이트 예제를 만들었다가 그대로 뮤텍스로 바꿔본거라 저모양입니다...;;

좋은 말씀 감사드립니다. 공부해야겠네요!

익명 사용자의 이미지

덧붙여서 물론 위 내용은 쓰레드 2개만을 기준으로 했을 때의 얘기입니다.

jick의 이미지

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

leadha의 이미지

링크주신 곳에 뮤텍스는 기본적으로 락을 잡고 있는시간보다 잡고있지 않는 시간이 길도록 코딩해야한다는 내용도 있군요.
생각해보면 효율상 당연한 이야기긴 한데,
FIFO 가 지켜지지 않는다는게 사실 어떠한 원리에서 가능한 일인지는 여전히 모르겠습니다.ㅠ
일단은 잘못을 알았네요!

감사합니다.

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.