탐구

24.01.12 동아리 Real Time Server 프로젝트로부터 - 1

트부 2024. 1. 12. 02:09
https://thebook.io/006884/

 
 
스레드 기본 사용법:

#include <iostream>
#include <thread>

void func1(int a, int b)
{
	std::cout << a + b << std::endl;
}

int main()
{
	std::thread thr1(func1, 1, 3);
	thr1.join();

	return 0;
}

스레드 생성자의 첫번째 인자로는 함수 포인터가 들어갈 수 있으므로 다음과 같이 사용할 수도 있다.
함수의 이름 자체는 배열처럼 포인터로 쓰이는 것은 문법을 어느정도 공부한 사람이라면 알고 있을 것이다.
 
스레드에 들어간 함수에 맞춰서 매개변수를 적어줘야 하며 그렇게 하지 않으면 오류가 난다.

스레드의 생성자

함수를 첫번째 인자로, 두 번째 인자로는 가변인자를 받는데 가변인자는 다음을 참고하자.
https://woo-dev.tistory.com/53

[C++] 가변 인자(Variable argument) 사용법을 알아보자

가변 인자(Variable argument)란 말 그대로 고정되지 않은 인자를 말한다. C언어의 printf() 함수처럼 정해지지 않은 개수의 인자를 받아야 할 때 사용한다. 가변 인자는 아래와 같이 인자를 ... 표시로

woo-dev.tistory.com

c언어에서 배운 printf와 scanf도 가변인자를 활용한 함수이다.
가변인자를 여러 자료형 조합으로 허용해서 받는 방법은 다음을 참고한다.
https://younggwan.tistory.com/54

C++ 가변 인자 템플릿

https://younggwan.tistory.com/53 C++ 가변인자 정의 매개 변수의 개수가 변할수 있는 인자이다. 예를 들어, printf, scanf 가 가변인자를 사용한 함수이다. 코드로 가변 인자를 사용하면서 설명하겠습니다.

younggwan.tistory.com

 



+c에서는 다음과 같이 사용한다.
https://norux.me/19

C언어 가변인자(가변파라미터)를 사용해보자

C언어 가변 인자(가변 파라미터)를 사용해보자 1. 가변인자란 무엇일까? printf 함수를 써보셨나요? 우리는 자연스럽게 printf("%d * %d = %d", 3, 5, 3*5)라고 쓰고 있습니다. 가만보면 printf라는 함수는 인

norux.me

https://velog.io/@kurtyoon/printf-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

[42] printf 구현하기

printf 구현하기

velog.io



 

&&는 우측값 레퍼런스라는 것이다.
클래스의 이동 생성자, 이동 대입 연산자 등과 관련이 있는데 Value category를 논해야 하는 것 같아서 다음에 다루겠다;;
대충 임시 객체를 전달할 때 임시 객체가 있던 메모리를 참조하는 대신에 임시 객체는 nullptr로 만들어 버린다.
이를 소유권을 이전한다고 표현하는 것 같다.
 

#include <iostream>
#include <thread>
#include <Windows.h>

class A {
public:
	int a;
};

void test(const A* aa)
{
	while (true)
	{
		std::cout << aa->a << '\n';
		Sleep(1000);
	}
}

int main()
{
	A newA;
	std::thread thr1(test, &newA);
	
	while (true)
	{
		std::cin >> newA.a;
	}

	return 0;
}

클래스 A의 포인터를 인자로 넘기면 외부에서 멤버 값을 변경했을 때 변경된 멤버를 참조할 수 있다.
 
 
join()은 스레드가 작업을 끝낼 때까지 기다리는 함수이기 때문에 스레드의 join함수를 쓰지 않는다면 비정상적인 종료가 되었을 때 호출되는 abort()가 호출될 수 있다.
(handled되지 않은 예외를 받아들이는 terminate에서 콜되는 듯하다.)
 
join을 쓰기 귀찮다면 jthread라는 것을 쓸 수가 있다.
main이 종료될 때 프로그램에 포함된 객체들도 함께 소멸되는데 jthread는 그 소멸자에 join을 달아놓았다.
 
또한 stop_token이라는 것을 지원한다.

#include <iostream>
#include <thread>
#include <Windows.h>

void func(std::stop_token tok)
{

	int i = 1;
	while (i++)
	{
		std::cout << i << ' ';
		if (tok.stop_requested())
		{
			std::cout << "stop\n";
			return;
		}
	}
}

int main()
{
	std::jthread jthr1(func);

	Sleep(3);

	return 0;
}

 
main이 종료될 때 jthread의 소멸자는 request_stop()과 join()을 호출한다.
request_stop()으로 stop_token의 종료 여부를 나타내는 변수를 바꿀 수 있는 것 같다.
이 경우 stop_token을 매개변수로 가진 함수여야만 가능한 것 같다. (매개변수 말고 지역변수 또는 전역으로 만들고 포인터로 전달시키면 토큰이 활성화되지 않는다.)
 
위와 같이 끝없이 실행되는 반복문을 포함한 경우에는 프로그램이 절대 끝나지 않는다.
이때 메인함수가 종료되었다는 일종의 이벤트를 줌으로써 프로그램을 종료시킬 수 있다.
 



+코드를 실행할 때마다 결과가 달라지는 것을 볼 수 있다.


 

(참고: https://velog.io/@dik654/memory-map)

 
왜 매개변수로 stop_token을 꼭 써야하며 생성할 때 stop_token타입의 매개변수를 전달 안해도 되는 것인가?
아마 stop_source 객체와 관련이 있는 것 같다.

stop_token을 안 쓰고 메인함수의 종료를 감지하는 방법은 다음과 같다.

#include <iostream>
#include <thread>
#include <Windows.h>

void func(std::stop_source sour)
{
	auto tok = sour.get_token();

	int i = 1;
	while (i++)
	{
		std::cout << i << ' ';
		if (tok.stop_requested())
		{
			std::cout << "stop\n";
			return;
		}
	}
}

int main()
{
	std::stop_source sour;
	std::jthread jthr1(func, sour);

	Sleep(3);

	sour.request_stop();

	return 0;
}

 
 request_stop()을 하면 stop_source와 stop_token의 stop state가 모두 바뀐다.
func의 매개변수로 참조형이나 포인터가 넘어가는 것도 아닌데 func 외부에서 값을 수정한게 어떻게 전달되는 건지 의문이 생겨서 고민하고 찾아보았다.
 
처음 내린 결론은 stop_source가 stop state를 나타낼 수 있는 멤버를 참조하고 stop_token이 stop_source를 참조하면, stop_source는 stop state를 바로 알 수 있고 stop_token은 한 다리 건너 그 상태를 알 수 있지 않냐는 거였다.
 
https://github.com/josuttis/jthread/blob/master/source/stop_token.hpp
 선배님 감사합니다.

공식 문서는 아닌 것 같지만 이 파일을 찾아보면 400번째 줄(stop_token)과 479번째 줄(stop_source)에

예상했던대로 어떤 객체를 참조하고 있다는 것을 알 수 있었다.
 
즉, stop_token과 stop_source는 굳이 참조형이나 포인터로 함수에 전달되지 않아도 외부의 stop state 변경에 반응할 수 있다. (싱글 스레드였다면 이는 불가능할 것이다.)
 
일반적인 매걔변수에 인자는 넘겨줘야 하는 반면에  stop_token에 관한 인자를 생성시에 안 넘겨줘도 된다.
아마 생성시에 타입 추론(템플릿 특수화 등)을 하고 매개변수가 stop_token형이라면 jthread 클래스 자체에서 토큰을 생성해서 넘겨 받은 함수와 bind해주는 거라고 생각했다.
 
https://github.com/josuttis/jthread/blob/master/source/jthread.hpp
98번째 줄에서 jthread의 생성자를 살펴볼 수 있다.

이니셜라이저로 stop_source와 thread를 초기화 해주며 함수가 직접적으로 하는 일은 없다.
 
if constexpr은 처음 보는 문법인데 주로 type_traits 헤더와 같이 쓰인다.
변수가 포인터인지 확인하는 등의 작업을 할 때 유용하다. (아마 템플릿 특수화를 쓰는 것 같다.)
컴파일 타입에 조건식을 검사해서 if가 유효하면 밑에 딸린 실행문만 남기고 나머지 조건식들은 무시, 조건이 거짓이면 뒤의 조건식들만 남긴다.
 
(참고: https://modoocode.com/293, constexpr의 전반적인 설명도 첨부가 되어있다.)

씹어먹는 C++ - <16 - 2. constexpr 와 함께라면 컴파일 타임 상수는 문제없어>

constexpr 을 통해 컴파일 타임 상수인 객체를 선언할 수 있다. const 와 constexpr 은 다르다. const 는 컴파일 타임에 상수일 필요가 없다! (const 인 애들 중에서 constexpr 이 있다고 생각하면 된다) constexpr

modoocode.com

 



+constexpr을 살피면서 재귀적인 구조체도 보게 되었다.

#include <iostream>
#include <functional>
#define Fac(x) Factorial<x>::value
using namespace std;

template <int N>
struct Factorial {
    static const int value = N * Factorial<N - 1>::value;
    //static struct Factorial<15> ff;
};
template <>
struct Factorial<0>{
    static const int value = 1;
};

template <int T>
int testFunc()
{
    return T;
}

template <typename Func>
void Ff(Func ff)
{
    cout << ff() << endl;
}

int main()
{
    int tt = testFunc<Factorial<10>::value>();
    cout << tt << endl;
    tt = testFunc<Fac(12)>();
    cout << tt << endl;

    Ff<int (void)>(testFunc<Fac(8)>);

    return 0;
}

저걸 어따 써먹을까 싶은데 혹시 몰라서 적어는 둔다.
Factorial 구조체 구번째 멤버는 static struct Factorial인데 static을 떼면 오류가 난다.
아마 정적이 아니라면 Factorial이 Factorial을 멤버로 계속 가지니까 무한대의 메모리가 필요해서 오류가 나는 듯하고 static을 붙이면 저 멤버마저 자기 자신을 멤버로 가지니 recusive하지 않다.
 
두번째 템플릿 구문이 템플릿 특수화이다.
 
위 코드는 constexpr이 생기기 전의 옛날 TMP방식이라고 한다.
 
tt변수가 값을 참조하는 testFunc의 템플릿 부분은 줄일 수 있을 것 같아서 define 전처리로 줄여보았다.
using 키워드로는 되지 않는 것 같다. (using Fac(p) = Factorial<p>::value)
 
템플릿으로 함수 형태(리턴타입, 매개변수 타입)가 들어가는 것도 확인해보았다. (Ff함수)
인자로 함수 포인터를 받는다. (맞나??)


 
 

다시 돌아와서, 아무튼 어떤 조건을 검사하고 나면 invoke함수로 stop_token을 전달해주는 것을 볼 수 있다.
그리고 예상했던대로 jthread 자체가 가지고 있는 stop_source가 get_token()으로 stop_token을 전달하는 것을 볼 수 있다.
 
forward함수는 퍼펙트 포워딩이라는 것과 관련있는 것 같은데 정확하게는 모르겠다.
 
decltype은 컴파일 타임에 타입을 추론해준다.
결국 <decltype(cb)>에서 cb의 타입이 int라면 <int>로 되는 것.
 
같이 쓰이는 녀석이 std::declval()이라는 녀석(utility 헤더에 있다)인데 클래스의 디폴트 생성자가 없다면 마치 있는 것처럼 만들어준다.
이게 무슨 말이냐면

https://modoocode.com/294

위와 같은 함수가 있을 때, T로 넘어온 어떤 클래스에 디폴트 생성자가 없을 때는 decltype(T().f())가 아니고 decltype(declval<T>().f()) 처럼 써야한다.
 
(참고: https://modoocode.com/294)

씹어먹는 C++ - <16 - 3. 타입을 알려주는 키워드 decltype 와 친구 std::declval>

decltype 키워드를 통해서 우리가 원하는 식의 타입을 알 수 있습니다. 만일 해당 식이 단순한 식별자 표현식 (identifier expression) 이라면 그냥 그 식의 타입으로 치환됩니다. 그 이외의 경우라면 해

modoocode.com

 


+c++에서 파이썬 처럼 함수를 작성할 수 있다는 것을 알았다.

template <typename T1, typename... Args>
auto func(T1 i, Args... args) -> decltype(i + args) {
	return i + func(args...);
}

이런식으로 쓰일 수가 있다.



 
처음에 스레드는 전달 받은 함수를 호출할 때 bind()로 함수와 인자를 묶고 호출하나보다 했는데 invoke()가 쓰여서 무엇인지 찾아보았다.
지금 새벽 4시... 글이 안 읽혀서 저명하신 GPT께 여쭤보았다.
 

#include <iostream>
#include <functional>

void regularFunction(int x) {
    std::cout << "Regular function: " << x << std::endl;
}

struct Functor {
    void operator()(int x) const {
        std::cout << "Functor: " << x << std::endl;
    }
};

struct MemberFunction {
    void memberFunction(int x) const {
        std::cout << "Member function: " << x << std::endl;
    }
};

int main() {
    int value = 42;

    // Using std::invoke with function pointer
    std::invoke(regularFunction, value);

    // Using std::invoke with functor
    Functor functor;
    std::invoke(functor, value);

    // Using std::invoke with member function pointer
    MemberFunction memberObj;
    auto memberFuncPtr = &MemberFunction::memberFunction;
    std::invoke(memberFuncPtr, memberObj, value);

    return 0;
}

제네릭 코드에서 유용하다는 거 보니까 단번에 이해가 간다.
 
bind도 비슷하게 사용할 수는 있다는데 invoke가 더 생산적인가 보다.
 
멤버 함수는 컴파일 단계에서 this포인터가 인자로 하나 더 들어간다는 것도 알게 되었다.
위 코드에서 멤버 함수 부분을 보면 객체가 인자로 하나 더 들어가는 것을 알 수 있다. (어떤 객체의 함수를 실행시킬 건지 알아야하니 당연한 것 같긴하다.)
 
스레드에 멤버 함수를 등록할 때도 this포인터를 넘겨준다.
 
(참고: https://boycoding.tistory.com/250, https://snowfleur.tistory.com/144)

[C++] 멤버 함수를 Thread에 등록하는 방법

개요 C++11의 표준 라이브러리인 Thread을 사용할 때 전역 함수가 아닌 Class 멤버 함수를 등록하고 싶을 때가 있다. 이럴 때 다음과 같은 방법으로 진행을 하면 손쉽게 만들 수 있다. 소스코드 1 2 3 4

snowfleur.tistory.com

C++ 09.10 - this 포인터

this 포인터 객체 지향 프로그래밍에서 가장 많은 질문 중 하나는 "클래스의 멤버 함수를 호출할 때 C++는 어떻게 호출할 객체(인스턴스)를 찾는가?" 이다. 이 질문에 대한 정답은 this라는 숨겨진

boycoding.tistory.com

 
 



 
스레드를 쓰면 레퍼런스 인자를 넘길 때는 특이한 방식으로 넘긴다.

#include <iostream>
#include <thread>

int sum(int& a, int& b)
{
	return a + b;
}

int main()
{
	int aa, bb;
	std::thread thr(sum, std::ref(a), std::ref(b));

	return 0;
}

 ref함수는 인자로 들어온 객체에 대한 reference_wrapper를 생성해준다.
 
Wrapper 클래스는 기본 타입에 대한 추가적인 작업을 하고 싶을 때 사용할 수 있다. (다양한 사용처는 모르겠다.)
 
(참고: https://cho001.tistory.com/161)

Wrapper 클래스

Wrapper class 회사 후임이 Wrapper class를 몰라 혹시 모르는 사람들을 위해 미리 정리해 보았다. C++을 쓰시면 많이 볼 수있는데, 래퍼 클래스는 간단하게 말하자면, 기본 자료형(int나 long)같은 데이터

cho001.tistory.com

 
https://en.cppreference.com/w/cpp/utility/functional/reference_wrapper
 

std::reference_wrapper - cppreference.com

template< class T > class reference_wrapper; (since C++11) std::reference_wrapper is a class template that wraps a reference in a copyable, assignable object. It is frequently used as a mechanism to store references inside standard containers (like std::ve

en.cppreference.com

멤버로 템플릿 포인터를 가지고 있으며 레퍼런스 형변환을 오버로딩 하고 있다.
이 연산자 오버로딩 때문에 레퍼런스를 매개변수로 하는 함수에다가 reference_wrapper를 넘겨도 단순 레퍼런스와 비슷하게 동작하는 것이다.
 

https://jabdon4ny.tistory.com/m/72

위 코드의 send_foo함수는 레퍼런스를 인자로 받는 foo함수를 실행시키는데 일반적인 변수를 인자로 넣으면 복사가 되기 때문에 foo함수에서 값을 수정해도 외부에서는 값이 바뀌지 않는다.
하지만 reference_wrapper로 전달하게 되면 포인터를 멤버로 가진다는 특성 때문에 복사가 되어도 항상 같은 메모리를 가르키게 된다.
따라서, foo()에서 값을 수정해도 외부에서 그를 확인할 수 있다.
 
reference_wrapper가 이렇게 구현되어 있기 때문에 invoke()나 bind()에 전달해도 레퍼런스 매개변수와의 호환에는 문제가 없는 것이다.
 
함수가 매개변수로 const T&를 받으면 reference_wrapper를 안 쓰고 변수를 그냥 넘겨줘도 실행이 된다.
아마 value category와 관련된거 같아서 다음 글에 다뤄보도록 해야겠다.
ref() 자매품인 cref()를 사용하면 위 코드와 같은 상황에서 const T& 인자를 넘겨주는데 활용할 수 있다.
 
reference_wrapper는 복사 가능한 참조자라고도 한다.
기본적으로 참조자는 한 번 초기화가 되고 나면 가르키는 대상을 바꿀 수가 없다.
rA가 a를 참조하고 rB가 b를 참조하고 있으면 rA = rB라는 식은 a가 b의 값을 갖게 하라는 의미가 되는 것이다.
하지만, reference_wrapper는 rA = rB라는 식에서 rA가 b를 가르키도록 할 수 있다.
본질은 포인터이기 때문에 포인터 변수의 값만 바꿔주면 다른 주소를 가르킬 수 있다.
 


 
 
스레드 생성시에는 실행 후 메모리에 올라가는 함수를 인자로 넘겨줄 수도 있으나 lambda 함수라는 것을 이용할 수 있다.
 
다른 언어에서 이미 본 사람이 있을 것이다. (본인은 Unity C#을 다루면서 그 존재를 확인했었다.)
 


+함수 객체라는 것을 연산자 오버로딩을 통해서 만들 수 있다.
https://blog.hexabrain.net/267

C++/STL 강좌 2편. 함수 객체(Function Object)

1. 함수 객체(Function Object)이번에는 함수 객체(Function Object)에 대해 알아보도록 하겠습니다. 함수 포인터에 이어 함수 객체는 어떠한 기능일까요? 쉽게 말하자면, 함수 객체는 객체가 함수처럼 동

blog.hexabrain.net

 
클래스의 멤버라는 장점은 일반 함수에 정적 변수를 선언하는 것으로 대체가 가능할 것 같았는데 오산이었다.
 
함수 객체는 OOP의 다양한 개념을 사용할 수 있다는 것, 속성이 다른 함수 객체(동일 클래스)를 여러 개 생성해서 다른 목적으로 쓸 수 있다는 것이 매우 크게 작용한다.
임시 객체로 스레드를 초기화 시켜줄 수도 있다. => thread(Function_Obj(), ...)
 
위에서 잠깐 쓰였는데 함수 객체도 스레드의 인자로 넘겨줄 수 있다.


 
람다함수는 다음을 참고하자.
https://blog.koriel.kr/modern-cpp-lambdayi-teugjinggwa-sayongbeob/

Modern C++ lambda의 특징과 사용법

lambda는 람다 표현식 또는 람다 함수, 익명 함수(anonymous function)로 불립니다. 그 성질은 함수 객체(functor)와 동일합니다. 그 이름처럼 몸통은 있지만 이름이 없는 함수입니다. 요즘 대부분의 프로

blog.koriel.kr

 
캡처라는 특성이 눈에 띄는 것 같다.
 
스레드의 초기화에 쓰는 방식은 함수, 함수 객체를 사용하는 방법과 똑같다.