Computer Science 기본 지식/운영체제

[C++ Thread] C++ Thread 생성하기

로파이 2021. 4. 24. 13:09

쓰레드란 Thread

프로세스를 구성하는 독립적인 실행을 보장하는 객체이다. 프로세스의 흐름을 더 작은 단위로 나누고 프로세스에 할당된 자원, 데이터 세그먼트, 힙에 할당된 객체 등을 공유하여 사용할 수 있다.

 

쓰레드는 독립적인 실행을 보장하므로 자신이 실행하는 함수가 존재한다. 함수의 실행을 위해 자동 변수(쓰레드 로컬) 변수를 사용하기 위한 스택 메모리를 사용하게 된다. 프로세스 단위가 작으므로 문맥 교환에 더 적은 비용이 들기 때문에 멀티 쓰레드 프로그래밍이 자주 사용된다.

 

운영체제는 여러 쓰레드를 실행하기 위해 CPU의 전체 하드웨어 실행 시간를 나누어 각 쓰레드에게 CPU 사용가능 시간을 부여한다. 이를 타임 퀀텀(time quantum)이라고 한다. 또한 운영체제의 스케줄러가 쓰레드 실행 순서를 결정하는데 이는 운영체제 마다 다르고 반드시 생성한 순서대로 실행이 되는 것도 아니다.

 

C++ Thread

리눅스의 pthread_create와 Windows의 CreateThread를 대신하여 사용할 수 있는 쓰레드 객체가 C++11 표준에 추가되었다. (C++) thread 객체는 각 운영체제가 지원하는 네이티브 함수(pthread_create, CreateThread)를 사용하여 바탕 쓰레드를 운영하며 높은 추상화 수준으로 C++ 표준 코드로 작성시 운영체제 이식성이 좋다는 장점이 있다. 

 

std::thread

en.cppreference.com/w/cpp/thread/thread

C++ 20에 정의되어 있는 thread 클래스의 일부분이다.

// C++ 20 정의
class thread { // class for observing and managing threads
public:
    class id;

    using native_handle_type = void*;

    thread() noexcept : _Thr{} {}
	//...
private:
    _Thrd_t _Thr;
}
  • id : 쓰레드 id를 지칭한다.
  • native_handle_type: 운영체제마다 다르게 구현하는 쓰레드를 사용하는데 이 바탕 쓰레드 핸들의 타입을 지칭한다.
  • _Thr: 실제 바탕 쓰레드의 핸들이다.

특수 멤버 함수

- 생성자와 소멸자

쓰레드 객체는 빈 생성자 혹은 함수와 그 인자들을 받아 생성할 수 있다.

  // 기본 생성자
  thread() noexcept : _Thr{} {}
  
  // public thread 생성자
  template <class _Fn, class... _Args, enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0>
    _NODISCARD_CTOR explicit thread(_Fn&& _Fx, _Args&&... _Ax) {
        _Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
    }

 // private start 함수
   template <class _Fn, class... _Args>
    void _Start(_Fn&& _Fx, _Args&&... _Ax) {
        using _Tuple                 = tuple<decay_t<_Fn>, decay_t<_Args>...>;
        auto _Decay_copied           = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
        constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{});

#pragma warning(push)
#pragma warning(disable : 5039) // pointer or reference to potentially throwing function passed to
                                // extern C function under -EHc. Undefined behavior may occur
                                // if this function throws an exception. (/Wall)
        _Thr._Hnd =
            reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));
#pragma warning(pop)

        if (_Thr._Hnd) { // ownership transferred to the thread
            (void) _Decay_copied.release();
        } else { // failed to start thread
            _Thr._Id = 0;
            _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
        }
    }

_Start함수는 함수 객체와 인자들을 받아 윈도우 운영체제 _beginthreadex 시스템 함수를 내부적으로 사용하고 있다. _beginthreadex는 쓰레드를 생성하고 생성자로 전달받은 함수와 인자들을 가지고 실행 흐름을 만든다.

 

- 복사/이동 관련

    thread(thread&& _Other) noexcept : _Thr(_STD exchange(_Other._Thr, {})) {}

    thread& operator=(thread&& _Other) noexcept {
        if (joinable()) {
            _STD terminate();
        }

        _Thr = _STD exchange(_Other._Thr, {});
        return *this;
    }

    thread(const thread&) = delete;
    thread& operator=(const thread&) = delete;

    void swap(thread& _Other) noexcept {
        _STD swap(_Thr, _Other._Thr);
    }

기본적으로 복사는 금지되어 있으며 이동 기능만 지원하고 있다.

 

옵저버 멤버 함수

  • joinable: 객체가 합류가능상태인지 반환한다.
  • get_id: 쓰레드의 id를 반환한다.
  • native_handle: 바탕 쓰레드의 핸들을 반환한다.
  • hardware_concurrency: CPU가 실행가능한 동시 쓰레드의 최대 갯수를 반환한다.

연산 멤버 함수

  • join: 해당 쓰레드의 함수가 종료될 때까지 기다린다.
  • detach: 쓰레드 실행 흐름을 독립적으로 분리한다.
  • swap: 두 쓰레드의 소유를 교환한다.

예시)

쓰레드는 호출가능한 모든 객체를 인자로 받아 실행 흐름을 실행할 수 있다.

  • 일반 함수
  • Functor
  • 람다
// function
void sum_func(int &sum, const std::vector<int> &v)
{
	for (const int& e : v)
	{
		sum += e;
	}
}

// Functor
class SumFunc
{
public:
	void operator()(int& sum, const std::vector<int>& v)
	{
		for (const int& e : v)
		{
			sum += e;
		}
	}
};

int main()
{
	std::vector<int> vec;
	for (int i = 0; i < 100; ++i)
		vec.push_back(i);
	
	int sum1 = 0, sum2 = 0, sum3 = 0;
	thread t_sum1(sum_func, std::ref(sum1), vec);
	thread t_sum2(SumFunc(), std::ref(sum2), vec);
	thread t_sum3([&sum3, &vec]() {for (const int& e : vec) { sum3 += e; }; }); // lambda

	t_sum1.join();
	t_sum2.join();
	t_sum3.join();

	printf("%d\n", sum1); //4950
	printf("%d\n", sum2); //4950
	printf("%d\n", sum3); //4950
    return 0;
}