경쟁 상태 : 여러 스레드가 공유 리소스를 동시에 접근.
테어링
데드락(교착 상태) : 경쟁 상태를 막기 위해 상호 배제와 같은 기법을 사용할 때 발생하는 문제.
잘못된 공유(false sharing) : 캐시는 x86의 경우 64바이트 캐시 라인 단위로 처리되는데, 캐시 라인에 데이터를 쓰려면 캐시 라인 전체를 lock해야함. 두 스레드가 두 가지 데이터 영역을 사용하는 데 동일한 캐시 라인에 있는 경우 성능이 떨어진다.
#include<thread>
함수 포인터로 스레드 만들기
#include <iostream>
#include <thread>
void func(int arg1, double arg2)
{
std::cout << "arg1 : " << arg1 << ", arg2 : " << arg2 << std::endl;
}
int main()
{
std::thread t1(func, 1, 2.0);
std::thread t2(func, 3, 4.0);
t1.join();
t2.join();
return 0;
}
함수 객체로 스레드 만들기
class C
{
public:
C(int arg1, double arg2)
: mArg1(arg1), mArg2(arg2)
{}
void operator()() const
{
std::cout << "arg1 : " << mArg1 << ", arg2 : " << mArg2 << std::endl;
}
private:
int mArg1;
double mArg2;
};
int main()
{
std::thread t1{C{1, 2.0}};
C c(3, 4.0);
std::thread t2(c);
t1.join();
t2.join();
return 0;
}
람다 표현식으로 스레드 만들기
int main()
{
int arg1 = 1;
double arg2 = 2.0;
std::thread t1([arg1, arg2]
{
std::cout << "arg1 : " << arg1 << ", arg2 : " << arg2 << std::endl;
});
t1.join();
return 0;
}
멤버 함수로 스레드 만들기
class C
{
public:
C(int arg1, double arg2)
: mArg1(arg1), mArg2(arg2)
{}
void threadMethod()
{
std::cout << "arg1 : " << mArg1 << ", arg2 : " << mArg2 << std::endl;
}
private:
int mArg1;
double mArg2;
};
int main()
{
C c(1, 2.0);
std::thread t1{&C::threadMethod, &c};
t1.join();
return 0;
}
스레드 취소하기 : 공유 변수를 사용해 값을 확인하며 중단 여부를 결정하는 것이 좋음. 공유 변수는 아토믹이나 조건 변수로 만드는 것이 좋음.
스레드 종료 후 최종 결과 가져오기
#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>
#include <vector>
using namespace std::chrono;
void func(std::atomic<int>& arg1)
{
for (int i = 0; i < 100; ++i)
{
++arg1;
std::this_thread::sleep_for(1ms);
}
}
int main()
{
std::atomic<int> arg1(0);
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i)
{
threads.push_back(std::thread{func, std::ref(arg1)});
}
for (auto& thread : threads)
{
thread.join();
}
std::cout << "arg1 : " << arg1 << std::endl; // -> 1000
return 0;
}
매 for 루프마다 arg1에 접근하기보다는 하나의 변수에 sum을 계산한 후에 마지막에 arg1에 sum을 더해주는 편이 더 좋다.
아토믹 연산
non timed mutex
std::mutex, recursive_mutex, shared_mutex(C++17)
메서드
recursive_mutex : lock()/try_lock()을 여러번 호출할 수 있다. 락을 해제하려면 unlock()을 lock()/try_lock() 횟수만큼 호출해야함.
shared_mutex : 읽기-쓰기 락.
timed mutex
RAII 원칙을 만족하는 클래스.
lock 클래스 소멸자는 확보한 mutex를 자동으로 unlock함.
std::lock_guard, unique_lock, shared_lock, scoped_lock(C++17)을 제공함.
lock_guard : 아래의 2가지 생성자를 지원.
unique_lock : owns_lock()을 통해 락이 걸렸는 지 확인 가능.
shared_lock : 위의 shared_mutex의 기능을 함. unique_lock 클래스와 동일한 인터페이스를 가진다.
lock()은 가변 인수 템플릿 함수로 여러 개의 뮤텍스 객체를 데드락 걱정 없이 한번에 lock을 걸 수 있다.
scoped_lock : 인수 개수에 제한이 없음 + lock_guard와 같이 스코프를 벗어나면 lock이 해제됨.
std::call_once(), std::once_flag : call_once의 인수로 지정한 함수/메서드는 단 한 번만 실행된다.
thread safe std::cout 예시
#include <thread>
#include <mutex>
#include <iostream>
class C
{
public:
C(int arg1, double arg2)
: mArg1(arg1), mArg2(arg2)
{}
void operator()() const
{
for (int i = 0; i < 100; ++i)
{
std::lock_guard<std::mutex> lock(sMutex);
std::cout << "arg1 : " << mArg1 << ", arg2 : " << mArg2 << std::endl;
}
}
private:
int mArg1;
double mArg2;
static std::mutex sMutex;
};
std::mutex C::sMutex;
int main()
{
std::thread t1{ C{1, 2.0} };
C c(3, 4.0);
std::thread t2(c);
t1.join();
t2.join();
return 0;
}
#include <thread>
#include <mutex>
#include <chrono>
#include <iostream>
using namespace std::chrono;
class C
{
public:
C(int arg1, double arg2)
: mArg1(arg1), mArg2(arg2)
{}
void operator()() const
{
for (int i = 0; i < 100; ++i)
{
std::unique_lock<std::timed_mutex> lock(sTimedMutex, 200ms);
if (lock.owns_lock())
{
std::cout << "arg1 : " << mArg1 << ", arg2 : " << mArg2 << std::endl;
}
}
}
private:
int mArg1;
double mArg2;
static std::timed_mutex sTimedMutex;
};
std::timed_mutex C::sTimedMutex;
int main()
{
std::thread t1{ C{1, 2.0} };
C c(3, 4.0);
std::thread t2(c);
t1.join();
t2.join();
return 0;
}