왜 필요?
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
void add (int & num)
{
for(int i=0;i <1000000;++i)
++num;
}
int main()
{
int num = 0;
vector<thread> threads;
for (int i = 0; i < 4; ++i)
threads.emplace_back(add, std::ref(num));
for (auto & thread : threads)
thread.join();
cout << num << endl;
}
위의 코드는 4개의 스레드에게 변수 num을 1씩 더하는 과정을 100만번 수행하도록 하고 있다.
뭐.. 400만이라는 숫자가 출력되어야 할 것 같지만
당연하게도 데이터 레이스 때문에 이상한 숫자가 출력된다.
아~ 그러면 뮤텍스를 써서 스레드 동기화를 해보자! 라는 생각이 든다.
#include <iostream>
#include <thread>
#include <vector>
#include <ctime>
#include <mutex>
using namespace std;
mutex mtx;
void add (int & num)
{
for(int i=0;i <1000000;++i)
{
lock_guard<mutex> lock{ mtx };
++num;
}
}
int main()
{
int num = 0;
vector<thread> threads;
auto start = clock();
for (int i = 0; i < 4; ++i)
threads.emplace_back(add, std::ref(num));
for (auto & thread : threads)
thread.join();
auto end = clock();
cout << num << endl;
cout << "time : " << (end - start) << "ms" << endl;
}
400만으로 결과가 잘 나오긴 하는데 시간이 1636ms나 걸렸다.
여기서 아토믹을 쓰면 속도를 개선할 수 있다. 다음 코드에선 변수 num을 atomic<int>로 선언하였다.
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
#include <ctime>
using namespace std;
void add (atomic<int> & num)
{
for(int i=0;i <1000000;++i)
{
++num;
}
}
int main()
{
atomic<int> num{ 0 }; //0은 초기화 값
vector<thread> threads;
auto start = clock();
for (int i = 0; i < 4; ++i)
threads.emplace_back(add, std::ref(num));
for (auto & thread : threads)
thread.join();
auto end = clock();
cout << num << endl;
cout << "time : " << (end - start) << "ms" << endl;
}
결과도 잘 나오고 속도도 훨씬 빠르다
atomic<T> 는 어떤 타입을 지원?
그렇다면 아토믹은 아무런 타입이나 다 지원해줄까? 아니다.
대충 다음을 만족하면 사용 가능하다.
1. 메모리 안에서 연속된 청크임
2. 오브젝트를 복사하는 것이 모든 bit을 복사하는 것과 같음 (memcpy)
3. 가상함수나 noexcept 생성자가 없음
그러니까 int, long, char, int*같은 타입은 당연히 가능하고 몇몇 클래스나 구조체도 가능하다는 것이다.
헷갈린다면 is_lock_free() 메서드를 통해 확인해볼 수 있다.
#include <atomic>
#include <iostream>
using namespace std;
struct SomeStruct1
{
int a, b, c;
};
struct SomeStruct2
{
int a[100];
int b;
};
int main()
{
atomic<SomeStruct1> ss1;
cout << ss1.is_lock_free() << endl; // 0 (false) 출력
atomic<SomeStruct2> ss2;
cout << ss2.is_lock_free() << endl; // 0 (false) 출력
atomic<int**> doubleptr;
cout << doubleptr.is_lock_free() << endl;// 1 (true) 출력
return 0;
}
어떤 연산을 지원하나?
대충 다음과 같이 일반 변수가 쓰는 연산들을 그대로 지원한다.
atomic<int> x{ 0 };
int y;
//동일한 코드
x.store(10);
x = 10;
//동일한 코드
y = x.load();
y = x;
//정수 타입 한정, 동일한 코드
x.fetch_add(y);
x += y;
++x;
x++;
x |= 2; //bit set
// x*=2; 곱셈은 지원 안 함 (컴파일에러)
그러나 다음과 같은 식들은 atomic에서는 조금 주의해야 한다.
atomic<int> x{0};
int y = x * 2; //atomic read (x)
x = y + 1; // atomic write (x)
x = x + 1; // x+=1과 다름!! atomic read and write임
x = x * 2; //이런 곱셈은 가능. 왜냐 read/write 이기 때문
더 복잡한 식들도 지원한다.
atomic<int> x{ 0 };
int y = x.exchange(10);
// y = x, x = 10; 과 같은 의미
bool result = x.compare_exchange_strong(y, 10);
// compare_exchange_strong 은 다음과 같은 의미이다.
//if(x==y)
//{
// x = 10;
// return true;
//}
//else
//{
// y = 10;
// return false;
//}
//weak한 compare_exchange는 strong보다 일반적으로 빠르지만 실패할 수 있다.
//그래서 다음과 같이 계속 시도하는 방법을 쓸 수 있다.
while(!x.compare_exchange_weak(y, 10))
{
}
왜 빠름?
그렇다면 atomic은 왜 빠를까?
atomic과 유사한 개념이 c#에서는 interlocked인거같은데 ..
https://tsyang.tistory.com/107
즉 mutex는 커널모드 동기화 요소이고, atomic은 유저 모드 동기화 요소 (CPU가 해주는 것)여서 빠르다고 할 수 있다.
CPU가 뭘해준다는 거?
위 글을 보면 Interlocked의 메서드들은 CPU 레벨에서 지원하는 연산(Instruction)이기 때문에 빠르다는 것이다. CPU는 내부적으로 이러한 연산을 사용할 때 변수에 락을 걸어 동시에 하나의 스레드만 접근할 수 있게 해준다.
반면에 뮤텍스는 커널단에서 일어나는 것이기에 느릴 수 밖에 없는 것.
'언어 > C++' 카테고리의 다른 글
아토믹으로 Lock-Free 자료구조 만들기 (0) | 2022.07.10 |
---|---|
Cpp 함수 (C++11 lambda, std::function) (2) | 2021.07.31 |
Cpp - 상속#2 (2) | 2021.07.22 |
Cpp - 상속#1 (0) | 2021.07.18 |
C++ (복사/이동) 생성자, 할당자, Rule of Three(Five) (0) | 2020.11.08 |