멀티쓰레드 애플리케이션 예제: native thread API

Jin Hur·2022년 1월 2일
0

reference: "Mastering C++ Multithreading" / 마야 포쉬

요약

  1. 네이티브 쓰레드 API를 사용해 C++에서 멀티쓰레드 애플리케이션에 관한 기본적 사항
  2. 여러 쓰레드가 한 작업을 병렬(parallel)로 수행하도록 하는 방법

멀티쓰레드 애플리케이션

가장 기본적인 형태의 멀티쓰레드 애플리케이션은 둘 또는 그 이상의 쓰레드를 가진 단일 프로세스로 구성.
이들 쓰레드는 다양한 방식으로 사용될 수 있는데, 예들 들어 프로세스로 하여금 발생하는 특정 이벤트를 하나의 쓰레드가 처리하게 하거나 여러 쓰레드에 작업을 분산해 데이터를 처리해 속도를 높이는 등의 상식으로 이벤트에 비동기적으로 응답하게 할 수 있다.

이벤트에 대한 비동기적 응답의 예
GUI 이벤트와 네트워크 이벤트를 별도의 쓰레드에서 처리한다.
상이한 두 이벤트 유형이 서로 대기할 필요가 없고, 두 이벤트 유형은 제 시간에 응답하는 이벤트를 서로 블록시키지 않도록 하는 것을 들 수 있다.
일반적으로 하나의 쓰레드는 GUI나 네트워크 이벤트를 처리한다거나 데이터를 처리하는 것처럼 하나의 작업을 수행한다.

기본적인 예의 경우 애플리케이션은 하나의 쓰레드로 시작한 다음 추가적인 쓰레드를 시작하고 이들이 작업을 마치기를 대기한다. 이들 새로운 각 쓰레드는 종료 전에 자신들의 작업을 완료한다.


1. 멀티쓰레딩 프로그래밍을 위한 헤더 파일들

#include <iostream> // for 표준출력 cout
#include <vector>   // for vector 자료구조
#include <random>   // 무작위 순서를 생성하는 클래스와 메서드 제공

// 멀티쓰레드 애플리케이션의 핵심
#include <thread>   // 쓰레드 생성을 위한 기본적 수단 제공
#include <mutex>    // 쓰레드 간에 thread-safe 상화 작용

using namespace std;

2. 전역변수들

// 전역변수
mutex values_mutex; // 전역 벡터용 뮤텍스
mutex cout_mutex;   // cout용 뮤텍스 (cout는 thread-safe 하지 않다.)
vector<int> values;

3. 쓰레드 수행 메서드

// 랜덤 넘버 생성 메서드
// 반환값의 범위를 정하는 두 인자를 가짐
int randGen(const int& min, const int& max){
    return rand()%10;
}

// 쓰레드가 수행할 메서드
void threadFunc(int tid){
    // cout 뮤텍스를 사용해 단 하나의 쓰레드만이 cout에 쓰기 작업 보장
    cout_mutex.lock();
    cout << "Starting thread " << tid << '\n';
    cout_mutex.unlock();

    // 벡터의 초기 설정값 구하기
    // 값을 지역변수에 복사 후 벡터에 대한 뮤텍스를 즉시 해제하도록 함. (다른 쓰레드를 위해서)
    values_mutex.lock();
    int val = values[0];
    values_mutex.unlock();

    // 생성된 쓰레드가 수행하는 핵심 작업
    // 쓰레드는 초기값을 받아서 이에 무작위로 생성된 값을 더함
    int rval = randGen(0, 10);
    val += rval;

    // 새로운 값을 벡터에 추가하기 전에 안전하게 로그로 기록
    cout_mutex.lock();
    cout << "Tread " << tid << " adding " << rval << ". New value: " << val << '\n';
    cout_mutex.unlock();

    // 벡터를 점유하고 새로운 값을 push
    values_mutex.lock();
    values.push_back(val);
    values_mutex.unlock();

    // 메서드 끝 지점까지 도달하면 쓰레드는 종료
    // 메인 쓰레드에서 재합류하기를 대기하는 쓰레드의 수가 하나 감소
    // 쓰레드의 합류는 자신을 생성한 쓰레드로 반환값을 전달하면서 더 이상 존재하지 않게 됨을 의미 

}

4. 메인함수

int main(){
    values.push_back(42);

    thread tr1(threadFunc, 1);
    thread tr2(threadFunc, 2);
    thread tr3(threadFunc, 3);
    thread tr4(threadFunc, 4);

    tr1.join();
    tr2.join();
    tr3.join();
    tr3.join();
}

Makefile

GCC := g++	# 사용할 컴파일러 정의

OUTPUT := ch01_mt_example	# 출력 바이너리 이름 지정

SOURCES := $(wildcard *.cpp)	# 소스 정보
# 와일드카드 기능, 폴더의 각 소스 파일 이름을 개별적으로 정의할 필요없이 \
와일드카드 다음에 나오는 문자열과 일치하는 모든 파일을 한 번에 수집

CCFLAGS := -std=c++11 -pthread	# 컴파일러 플래그 수집 정보
# c+11 기능 활성화, pthread 라이브러리 링킹

all: $(OUTPUT) # all 메서드, make로 하여금 제공된 정보와 함계 g++를 실행하도록 알려줌 
	
$(OUTPUT):
	$(GCC) -o $(OUTPUT) $(CCFLAGS) $(SOURCES)
	
clean:
	rm $(OUTPUT)
	
.PHONY: all

결과

출력 -> 추가 -> 출력 -> 추가 -> .. (동기적)이 아닌 다소 비동기적인 특성을 갖는다.


그 밖에 애플리케이션

위 예제는 데이터나 작업을 병렬적으로 처리해야 하는 애플리케이션에 유용하다.
예를 들어 비즈니스 로직과 네트워크 관련 기능을 가진 GUI 기반의 애플리케이션이라면, 필요한 쓰레드를 시작하는 주 애플리케이션의 기본 설정은 동일하게 유지되겠지만, 각 쓰레드는 동일하지 않고 완전히 다른 메서드가 될 수 있다.

메인 쓰레드는 GUI와 네트워크, 비즈니스 로직 쓰레드를 시작한다. 여기서 비즈니스 로직 쓰레드는 데이터를 주고받기 위해 네트워크 쓰레드와 통신을 한다. 비즈니스 로직 쓰레드는 또한 사용자 입력을 GUI 쓰레드로부터 받으며 갱신된 값을 GUI에 표시하도록 되돌려 준다.

0개의 댓글