POSIX Thread 스레드 프로그래밍 C/C++

POSIX Thread 스레드 프로그래밍 C/C++

Thead

Thread 란 프로세스 내에서 실행되는 흐름의 단위를 말한다.

최근 소켓을 하면서 Thread 프로그래밍이 필요한 경우가 생겨서 공부하고 글로 남긴다. Server 단에서 accept() 함수, recv() 함수 또는 Client 단에서 connect() 함수, send() 함수는 System call을 하고 응답이 와야 다음으로 진행하는 “Block”함수이다.

서버-클라이언트 간 1:1 통신에서는 서로 한번씩 밖에 주고받을 수 없으며, 만약 클라이언트 간 다중 통신을 해야한다면 위와 같은 block 함수가 문제일 수 있다.

 

POSIX란

POSIX(포직스/ˈpɒzɪks/)는 이식 가능 운영 체제 인터페이스(移植可能運營體制 interface, portable operating system interface)의 약자로, 서로 다른 UNIX OS의 공통 API를 정리하여 이식성이 높은 유닉스 응용 프로그램을 개발하기 위한 목적으로 IEEE가 책정한 애플리케이션 인터페이스 규격이다.

위키피디아에 따르면 위와 같다고 한다. 시스템마다 제공하는 API가 다르면 사용하기 불편하여 IEEE에서 표준으로 정한 것이라고 한다. 이 POSIX API에서 스레드를 생성하고 실행하는 API가 “pthread.h”에 정의되어 있다.

 

pthread.h

다양한 C/C++ 컴파일러(MinGW GCC, Cygwin 등)에 위의 POSIX API 가 이미 포함되어 있어 include 후 사용하면 된다.

 

Thread 생성하기

thread 를 생성하기 위해 다음과 같은 구조체 변수/함수가 사용된다.

  • 구조체 변수 pthread_t
  • 구조체 생성 함수 pthread_create( 스레드변수, 옵션, 실행할 함수명, 매개변수 )

또한 thread 로 실행할 함수는 void *FUNCTION(void *)형으로 선언/정의가 되어야 한다.

#include <iostream>
#include <pthread.h>
#include <string>

using namespace std;

/* 콘솔에 출력하는 함수 */
void out(string);

/* 스레드가 사용할 전역변수 */
int cnt = 0;

/* 스레드로 실행할 함수 */
void *doSomething(void *);

int main()
{
    /* 구조체 변수 선언 */
    pthread_t thread;

    /* 구조체 생성 */
    pthread_create( &thread, NULL, doSomething, NULL );

    /* 스레드에서 변경한 cnt 변수 출력 후 종료 */
    out( to_string(cnt) );

    return 0;
}

void out(string str)
{
    cout << str << endl;

    return;
}


void *doSomething(void *param)
{
    int i;

    for( i = 0 ; i < 50000 ; i++ )
    {
        cnt++;
    }

    return NULL;
}

결과는 thread 로 돌린 doSomething() 함수에서 50000이 출력되어야 의도가 맞는 프로그램일 것이다.

result
result

결과는 0으로 나왔다. 이는 스레드를 만들고 실행되기 전 프로그램이 끝났다는 소리가 된다.

그렇다면 프로그램이 끝나기 전 스레드가 생성부터 종료까지 대기할 수 있는 방법은 없을까?

Thread 종료 대기

위와 같은 문제는 아래 함수로 thread 가 종료될 때 까지 대기하는 함수를 사용하면 된다.

  • pthread_join()

#include <iostream>
#include <pthread.h>
#include <string>

using namespace std;

/* 콘솔에 출력하는 함수 */
void out(string);

/* 스레드가 사용할 전역변수 */
int cnt = 0;

/* 스레드로 실행할 함수 */
void *doSomething(void *);

int main()
{
    /* 구조체 변수 선언 */
    pthread_t thread;

    /* 구조체 생성 */
    pthread_create( &thread, NULL, doSomething, NULL );
    
    /* 스레드가 종료될 때 까지 대기하는 함수 */
    pthread_join( thread, NULL );

    /* 스레드에서 변경한 cnt 변수 출력 후 종료 */
    out( to_string(cnt) );

    return 0;
}

void out(string str)
{
    cout << str << endl;

    return;
}


void *doSomething(void *param)
{
    int i;

    for( i = 0 ; i < 50000 ; i++ )
    {
        cnt++;
    }

    return NULL;
}

결과는 의도한 것과 같이 50000이 나왔다.

result
result

기본적인 스레드의 생성-종료는 두 개의 메서드로 간단하게 구현이 가능하다. 글쓴이도 이정도면 충분히 구현 가능하겠다는 생각을 했지만 그렇게 쉽지 않았다.

문제점은 여러 스레드가 하나의 공용변수(위 코드에서 cnt)에 접근했을 때 일어났으며, 의도한 바와 다르게 출력이 됐다.

 

여러 스레드가 하나의 변수에 접근

위 코드에서 같은 메서드를 스레드 2개로 만들어 cnt에 접근하도록 했다.

#include <iostream>
#include <pthread.h>
#include <string>

using namespace std;

/* 콘솔에 출력하는 함수 */
void out(string);

/* 스레드가 사용할 전역변수 */
int cnt = 0;

/* 스레드로 실행할 함수 */
void *doSomething(void *);

int main()
{
    /* 구조체 변수 선언 */
    pthread_t thread1;
    pthread_t thread2;

    /* 구조체 생성 */
    pthread_create( &thread1, NULL, doSomething, NULL );
    pthread_create( &thread2, NULL, doSomething, NULL );

    /* 스레드가 종료될 때 까지 대기하는 함수 */
    pthread_join( thread1, NULL );
    pthread_join( thread2, NULL );

    /* 스레드에서 변경한 cnt 변수 출력 후 종료 */
    out( to_string(cnt) );

    return 0;
}

void out(string str)
{
    cout << str << endl;

    return;
}


void *doSomething(void *param)
{
    int i;

    for( i = 0 ; i < 50000 ; i++ )
    {
        cnt++;
    }

    return NULL;
}

50000씩 두 번, 100000이 나와야 할 것 같지만 이상하게 실행할 때 마다 다른 값이 출력되는 것을 볼 수 있을 것이다.

result
result
result
result

서로 다른 스레드가 서로 다른 시간에 cnt 에 접근하여 1을 추가하면 별 일이 없을 것이다. 하지만 두 개의 스레드는 서로 다른 흐름을 갖고 있고, 꼭 다른 시간에 cnt를 접근하라는 법은 없다.

따라서 위와 같은 경우는 두 개의 스레드가 같은 시간에 cnt에 접근하여 1을 올렸다는 의미가 될 것이다. 첫 번째 같은 경우는 35144번 같은 시간에 접근하여 두 스레드가 최종 1을 올렸다는 의미가 된다.

그렇다면 1번 스레드가 해당하는 변수 cnt를 사용할 때 다른 스레드는 접근을 못하도록 만들면 될 것이다.

 

임계구역 설정

여러 스레드가 공용 변수에 접근하는 부분을 Critical section(임계구역)이라고 한다. 위에서 말 했다 시피 한 스레드가 사용중일 때 다른 스레드가 사용하지 못 하도록 잠근다면(lock) 다른 스레드는 공용 변수를 사용하기 위해 대기할 것이고, 사용중이던 스레드가 잠금을 해제한다면(unlock) 대기중이었던 스레드는 해당 공용 변수를 사용할 것이다.

잠금을 위해 자물쇠(Mutex)를 선언 및 초기화 한 후 사용한다.

  • pthread_mutex_t
  • pthread_mutex_lock()
  • pthread_mutex_unlock()

#include <iostream>
#include <pthread.h>
#include <string>

using namespace std;

/* 콘솔에 출력하는 함수 */
void out(string);

/* 스레드가 사용할 전역변수 */
int cnt = 0;

/* pthread mutex 초기화 */
pthread_mutex_t mutex;

/* 스레드로 실행할 함수 */
void *doSomething(void *);

int main()
{
    /* 구조체 변수 선언 */
    pthread_t thread1;
    pthread_t thread2;

    /* pthread mutex 초기화 */
    pthread_mutex_init( &mutex, NULL );

    /* 구조체 생성 */
    pthread_create( &thread1, NULL, doSomething, NULL );
    pthread_create( &thread2, NULL, doSomething, NULL );

    /* 스레드가 종료될 때 까지 대기하는 함수 */
    pthread_join( thread1, NULL );
    pthread_join( thread2, NULL );

    /* 스레드에서 변경한 cnt 변수 출력 후 종료 */
    out( to_string(cnt) );

    return 0;
}

void out(string str)
{
    cout << str << endl;

    return;
}


void *doSomething(void *param)
{
    int i;

    for( i = 0 ; i < 50000 ; i++ )
    {
        pthread_mutex_lock( &mutex );
        cnt++;
        pthread_mutex_unlock( &mutex );
    }

    return NULL;
}

doSomething() 내의 cnt 변수를 사용하는 부분은 두 스레드가 접근하는 Critical section 이 될테고, 이 부분을 자물쇠로 잠그고 풀고 하면 된다.

결과는 의도한 것 처럼 100000이 출력되는 것을 확인할 수 있다.

result
result

%d 블로거가 이것을 좋아합니다: