(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;
}