Post

(C++) thread는 context가 필요하다.

전역 함수를 threadMain으로 지정해 실행하는건 그냥 다른 언어 처럼 하면 되는데, 객체의 메서드를 threadMain으로 지정하는건 몇 가지 신경써야 할 부분이 있다. 보통 다른 언어 같으면, Thread 클래스 상속받아서 t.run() 해주면 끝인데, C++은 그렇지 못하다.

구식 방법

원래 클래스 내부의 메서드를 threadMain으로 지정하기 위해서는, threadMain가 될 메서드를 static으로 빼고, static 메서드는 클래스 내부의 일반 멤버에 접근하지 못하기 때문에 일반 메서드의 wrapper처럼 사용하고, 진짜 _threadMain은 일반 메서드로 구성한 다음, 그 일반 메서드를 호출하기 위해 threadMain 메서드에서 void\* 형을 인자로 받은 다음에 클래스로 형변환하고, 그 클래스 포인터를 이용해 _theadMain을 호출하는 번거로운 방법을 사용해야 한다.

bind를 사용하는 방법.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <functional>
#include <thread>

class TestCls {
public:
	int val;
	void threadMain(int d) {
		printf("[1.val] = %d\n", val);
		std::this_thread::sleep_for(std::chrono::seconds(2));
		printf("[2.val] = %d\n", val);
	}
};


int main()
{
	TestCls test_cls;
	test_cls.val = 1;

	// [Error]   std::thread cls_thread(test_cls.threadMain, 3);
	/* 안된다. 생각해보면 되면 안된다.
	객체는 상태를 가지고 있는데 이런 식으로 넘기면 
	test_cls.threadMain든, test_cls2.threadMain든 메서드의 주소 정보만 넘어가기 때문에
	test_cls의 context에 대한 정보는 넘어가지 않는다. 
	threadMain라는 함수의 내부에서 클래스의 다른 멤버 변수를 참조한다면
	어떤 객체의 멤버 변수를 참조해야할지 알 수 없어진다.
	그래서 1. 실행할 메서드 2. 컨텍스트(객체) 3. 인자 이렇게 넘겨줘야 한다.  */

	std::thread copied_ctx_t(std::bind(&TestCls::threadMain, test_cls));
	std::thread refer_ctx_t(std::bind(&TestCls::threadMain, &test_cls));
	std::this_thread::sleep_for(std::chrono::seconds(1));
	test_cls.val = 4;
	printf("set val = 4\n");
	/* 이 때, context가 참조로 넘어가는지, 값으로 넘어가는지도 중요하다. 
	thread에 context를 넘기고 나서 context 객체의 변수 val이 수정될 수 있다.
	값으로 넘기게 되면, 객체의 현재 상태를 박제해서 이 context 하에서 threadMain이 실행되고,
	참조로 넘기게 되면, thread 코드가 실행되는 context가 변경되었을 때 반영된다. */

	copied_ctx_t.join();
	refer_ctx_t.join();

    return 0;
}
1
2
3
4
5
[1.val] = 1
[1.val] = 1
set val = 4
[2.val] = 1
[2.val] = 4
1
std::thread no\_bind\_t(&TestCls::threadMain, test\_cls);

사실 굳이 bind 붙여주지 않아도 내부적으로 알아서 해주는 듯. boost::thread의 경우 이렇게 넘기면 bind()로 알아서 감싸서 함수와 인자들이 internal storage로 복사된다고 나와있다. std::thread도 boost에서 상당 부분 참고해서 만들었다고 알고있다. 대충 비슷하게 돌아갈 것 같다.

!!!단, 이렇게 했을 때 안되는 경우가 있음.

boost::thread t(&boost::asio::io\_context::run, &\_io_context);는 bind하면 되고 이건 안되더라. static_cast하면 이렇게 써도 되긴 되는데, bind는 static_cast안써도 잘 되고.

람다를 사용하는 방법★

bind()는 몇 가지 단점을 가지고 있어 람다를 사용하는게 좋다. 단점 중 하나가 overloaded function에 대해서 사용하면 어떤 함수를 바인드해야할지 알 수 없기 때문에, static_cast<> 해서 딱 지정해줘야한다는 점이다. 이게 상당히 귀찮다.

bind를 사용했던 코드는 이렇게 바꿀 수 있고,

1
2
std::thread lambda\_ref\_thread( [&] { test\_cls.threadMain(); });
std::thread lambda\_copy\_thread([=] () mutable { test\_cls.threadMain(); });

threadMain(int) 함수가 오버로딩되었다면, 그냥 이렇게 써주면 된다. bind보다 훨씬 편하다.

1
2
std::thread lambda\_ref\_thread( [&] { test\_cls.threadMain(2); });
std::thread lambda\_copy\_thread([=] () mutable { test\_cls.threadMain(2); });

이런 식으로 Wrapping도 가능하지만…

굳이 이렇게 할 필요 없는 듯.
이렇게도 쓸 수 있긴 한데, 그냥 위처럼 람다로 class member function 하나 지정해서 스레드 생성해서 쓰는게 나을 것 같다.
std::thread가 이미 잘 되어 있는데 굳이 wrapping 할 필요가.
그리고 중괄호 내에 직접 thread에서 실행할 코드를 넣을 수 있도록 쓰는 문법이 다른 언어(코틀린이라던가.)에서도 지원되는 문법이라, 람다를 쓰는게 더 나은 측면도 있고.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <thread>
#include <atomic>

class UmbumThread {
private:
	std::thread worker;
	std::atomic<bool> running;
	int val;
protected:
	void run() {
		printf("[1.val] = %d\n", val);
		std::this_thread::sleep_for(std::chrono::seconds(1));
		printf("[2.val] = %d\n", val);
	}
public:
	UmbumThread(int _val) 
		: val(_val), running(false) { }
	
	void start() {
		running = true;
		worker = std::thread([this]() { this->run(); });
	}
	void stop() {
		// 이를 호출한 (main)thread는 그 부분에서 스레드 종료할 때 까지 blocking됨.
		if (worker.joinable()) {
			running = false;
			worker.join();
		}
	}
	virtual ~UmbumThread() {
		stop();
	}
};


int main()
{
	UmbumThread t(1);
	t.start();
	std::this_thread::sleep_for(std::chrono::seconds(3));
	t.stop();
    return 0;
}
This post is licensed under CC BY 4.0 by the author.