std::thread 생성
윈도우에 종속적인 API를 사용할수 있지만 C++11에서 추가된 std::thread
를 이용해서 thread를 생성하는 것을 추천한다.
#include <thread>
void HelloThread()
{
cout <<" Hello Thread" << endl;
}
int main()
{
// thread 생성
std::thread t(HelloThread);
cout << "Hello Main" << endl;
// 이외에 여러함수가 지원됨.
int32 count = t.hardwhare_concurrency(); // CPU코어 개수를 리턴
auto id = t.get_id(); // thread id
t.join(); // thread를 대기해준다.(이걸사용하지 않을경우 main이 먼저종료되어 Error처리됨)
t.detach(); // main thread와 연결을 끊는다.(거의 사용안됨.)
t.joinable(); // 연결(main thread에서 대기가능)이 가능한지 확인
}
여러 thread동시에 생성해보기
// 배개변수 받기, thread여러개 생성해보기
void HelloThread(int num)
{
cout <<" Hello Thread" << num << endl;
}
int main()
{
vector<std::thread> v;
for(int32 i = 0; i < 10; i++)
{
v.push_back(std::thread(HelloThread, i));
}
for(int32 i = 0; i < 10; i++)
{
if(v[i].joinable())
v[i].join();
}
}
Atomic
// 힙 or 데이터 영역의 변수
int32 sum = 0;
void Add()
{
for(int32 i = 0; i < 1'000'000; i++)
{
sum++;
}
}
void Sub()
{
for(int32 i = 0; i < 1'000'000; i++)
{
sum--;
}
}
int main()
{
std::thread t1(Add);
std::thread t2(Sub);
t1.join();
t2.join();
cout << sum << endl;
// 0이 안나오게 된다. -> Thread사용의 주의사항
}
#include <atomic>
// 여러 Thread에서 접근하는 변수는 이렇게 선언
atomic<int32> sum = 0;
void Add()
{
for(int32 i = 0; i < 1'000'000; i++)
{
sum.fetch_add(1);
}
}
void Sub()
{
for(int32 i = 0; i < 1'000'000; i++)
{
sum.fetch_add(-1);
}
}
int main()
{
std::thread t1(Add);
std::thread t2(Sub);
t1.join();
t2.join();
cout << sum << endl;
// 0이 나온다
}
단, atomic은 연산이 느리기에 절대적으로 필요한 경우에만 사용하자
std::mutex
atomic보다 좀 더 효율적인 방법을 알려준다.
vector<int32> v;
void Push()
{
for(int32 i = 0; i < 10'000; i++)
{
v.push_back(i);
}
}
int main()
{
std::thread t1(Push);
std::thread t2(Push);
// crash!! -> vector는 multi thread환경에 safe하지 않다
// vector는 공간이 부족할경우 메모리를 새로할당 받는데 그때 crash발생
t1.join();
t2.join();
cout << v.size() << endl;
}
해결법
#include <mutex>
vector<int32> v;
mutex m;
void Push()
{
for(int32 i = 0; i < 10'000; i++)
{
m.lock();
v.push_back(i);
m.unlock();
// 하지만 이 방식은 동작속도가 매우 느리다. -> Mutual Exclusive(상호배타적)
// 그리고 Deadlock을 조심해야한다.
}
}
int main()
{
std::thread t1(Push);
std::thread t2(Push);
t1.join();
t2.join();
cout << v.size() << endl;
}
조금 더 안전한 코드를 만들어 보자.
#include <mutex>
vector<int32> v;
mutex m;
void Push()
{
for(int32 i = 0; i < 10'000; i++)
{
std::lock_guard<std::mutex> lockGuard(m);
// or
std::unique_lock<std::mutex> uniqueLock(m);
// std::unique_lock<std::mutex> uniqueLock(m, std::defer_lock);
// std::defer_lock : 당장은 잠그지말고 .lock()을 호출하면 잠궈달라
v.push_back(i);
}
}
int main()
{
std::thread t1(Push);
std::thread t2(Push);
t1.join();
t2.join();
cout << v.size() << endl;
}
Deadlock 예시
lockguard로 모든 데드락을 방지할순 없다.
아래의 예시를 살펴보자.
class User
{
// ...
};
class UserManager
{
public:
static UserManager* Instance()
{
static UserManager instance();
return &instance;
}
User* GetUser(int32 id)
{
lock_guard<mutex> guard(_mutex);
return nullptr;
}
void ProcessSave()
{
lock_guard<mutex> guard(_mutex);
Account* account = AccountMnager::Instance()->GetAccount(100);
// ...
}
private:
mutex _mutex;
};
class Account
{
// ...
};
class AccountManager
{
public:
static AccountManager* Instance()
{
static AccountManager instance();
return &instance;
}
Account* GetAccount(int32 id)
{
lock_guard<mutex> guard(_mutex);
return nullptr;
}
void ProcessLogin()
{
lock_guard<mutex> guard(_mutex);
User* user = UserManager::Instance()->GetUser(100);
// ...
}
private:
mutex _mutex;
};
void Func1()
{
for(int32 i = 0; i < 100; i++)
{
UserManager::Instance()->ProcessSave();
}
}
void Func2()
{
for(int32 i = 0; i < 100; i++)
{
AccountManager::Instance()->ProcessLogin();
}
}
int main()
{
std::thread t1(Func1);
std::thread t2(Func2);
t1.join();
t2.join();
}
어떤상황에 Deadlock이 발생할까
ProcessSave()
, ProcessLogin()
이 호출될 경우 lock이 발생하고 ProcessSave()
, ProcessLogin()
내부에서 GetAccount
, GetUser
호출시 역시 lock이 발생하며 Deadlock이 된다.
해결책?? -> mutex lock을 잠그는 순서를 지정한다.
void ProcessLogin()
{
User* user = UserManager::Instance()->GetUser(100);
// 아래서 lock
lock_guard<mutex> guard(_mutex);
// ...
}