C++ Chapter 19.4 : 레이스 컨디션, std::atomic, std::scoped_lock
카테고리: Cpp
태그: Cpp Programming
인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀
🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!
chapter 19. 모던 C++ 필수 요소들
레이스 컨디션, std::atomic, std::scoped_lock
🔔 레이스 컨디션(Race Condition)이란
Race Condition 👉 동일한 데이터를 서로 다른 여러 스레드들이 접근하는 과정에서 생기는 문제
멀티 스레딩
은 여러개의 스레드들이 하나의 메모리를 공유하기 때문에 오류가 발생활 확률이 높다. 👉레이스 컨디션
- 접근 하는 순서에 따라 데이터 또는 결과값이 수정되고 그 수정된 데이터에 다른 데이터가 접근하려 잘못된 결과가 발생할 수 있다.
레이스 컨디션이 발생하는 과정
#include <iostream>
#include <mutex>
#include <atomic>
#include <thread>
using namespace std;
int main()
{
int shared_memory(0);
auto count_func = [&](){
for (int i = 0; i < 1000; ++i)
{
std::this_thread::sleep_for(std::chrono::microseconds(1));
shared_memory++;
}
};
thread t1 = thread(count_func);
thread t2 = thread(count_func);
t1.join();
t2.join();
cout << "After" << endl;
cout << shared_memory << endl;
}
💎출력💎
After
1962
0 초기 값을 가진
shared_memory
라는 변수에 열 스레드들이 동시에 접근하도록 해볼 것이다.
count_func
는 1 밀리세컨즈 대기하고난 후shared_memory
를 1만큼 증가시키는 일을 하는 함수다.- 모든 외부 변수를 레퍼런스로 받는
[&]
람다 함수이므로shared_memory
값도 변하게 된다. sleep_for
함수를 두어 대기 시간을 가진 이유는 대기 시간 없으면shared_memory++;
연산이 너무 단박에 빨리 끝나버려서 문제가 안보일 수 있어서
- 모든 외부 변수를 레퍼런스로 받는
t1
,t2
스레드는 각자count_func
작업을 수행한다.
📢 레이스 컨디션 발생 !
결과적으로 shared_memory
값은 최종적으로 2000 이 될 것으로 기대가 되지만 실제론 1962 가 나왔다! 다시 실행해보면 1968이 나오는 등 실행 때마다 2000 과는 다른 값이 나오게 된다.
OS 는 CPU가 어떤 스레드를 실행할지에 대한 스케줄링을 마음 대로 결정한다.
- A 스레드를 작업하는 중간에도 B 스레드를 작업하러 가고 그럴 수 있음
- 그래야 병렬적으로 처리 되는 것처럼 보이니까!
shared_memory++;
연산시 다음과 같은 3 단계로 진행이 된다.
- 1 단계 👉 메모리에 있는
shared_memory
값을 CPU로 보낸다. CPU가 데이터를 읽어들임! - 2 단계 👉 CPU 안에서 값을 +1 더하는 연산을 한다
- 3 단계 👉 더한 결과값을 다시
shared_memory
에 덮어씌운다
- 현재
shared_memory
값이 624 라고 가정해보자. t1
스레드가 1, 2 단계를 마치고 3단계로 가기 전 OS 는 sleep_for 대기 시간을 끝낸t2
스레드의 작업을 처리해준다.- 아직
t1
의 연산 결과인 625를shared_memory
에 덮어쓰지 못한 상태에서t2
로 넘어감 - 여전히
shared_memory
의 값은 624 인 상태.
- 아직
t2
가 잽싸게 1, 2, 3 단계를 모두 끝냈다고 가정해보자.shared_memory
은t1
스레드가 625를 덮어 씌우지 못했어서 여전히 624 이므로 이 값으로 1, 2, 3 단계를 진행하여shared_memory
는 625가 되었다.
- 다시 못다한
t1
스레드의 3 단계 작업으로 돌아왔다.- 스레드들은 같은 메모리를 공유하지만 각자 사용하는 CPU내의 레지스터는 별개이다. 따라서
t1
스레드의 연산 결과였던 625는 CPU 레지스터 내에 보관되어 있는 상태. - 2 단계인 CPU 연산 결과였던 625 를
shared_memory
에 덮어 씌운다. - 이미
shared_memory
는t2
스레드의 작업으로 625 가 된 상태였으므로 그대로 625 다.
- 스레드들은 같은 메모리를 공유하지만 각자 사용하는 CPU내의 레지스터는 별개이다. 따라서
- 두 스레드의 작업으로 624 👉 626 이 될 것으로 기대했지만 정작 624 👉 625 가 됐다.
- 덧셈 연산 하나가 무시된 꼴! 그래서 위에서 2000이 나오지 않고 1968 같은 더 적은 값이 나오게 된 것이다.
- 정리하자면
t1
스레드의 작업 도중에t2
스레드가 개입 되었기 때문에 이런 문제가 발생한 것이다. - 한 스레드가 작업 하는 도중에 다른 스레드가 동일한 메모리에 접근했기 때문
sleep_for 함수가 없다면 2000으로 결과가 올바르게 나오지만 그렇다고 해서 올바르게 동작했다는 것은 아니다. 너무 너무 너무 빠르게 for문을 1000번 다 돌아버려서 t2
스레드가 개입하기도 전에 t1
스레드가 for문 1000번을 다 돌아버리기 때문에 그런 것이다! 따라서 이런 레이스 컨디션 문제를 관측하기 위해 잠깐 잠깐씩 대기 시간을 두었다.
🔔 레이스 컨디션 해결 방법
std::atomic
#include <atomic>
- 아래 3단계를 꼭 한번에 다 실행되게끔 묶어준다. 쪼개질 수 없는 원자처럼!
- 1 단계 👉 메모리에 있는
shared_memory
값을 CPU로 보낸다. CPU가 데이터를 읽어들임! - 2 단계 👉 CPU 안에서 값을 +1 더하는 연산을 한다
- 3 단계 👉 더한 결과값을 다시
shared_memory
에 덮어씌운다
- 1 단계 👉 메모리에 있는
- 사용 방법
- 메모리가 공유되는 변수를
atomic
타입으로 설정하면 된다.atomic<int> shared_memory(0);
atomic
에 증감 연산자 같은 여러 연산자들이 오버로딩 되어있기 때문에shared_memory++
할 수 있다.- 혹은
shared_memory.fetch_add(1)
- 혹은
- 메모리가 공유되는 변수를
- 단점
- 느리다!
atomic
의 연산은 당연 일반 증감연산보다 느리다. 남용하면 엄청 느리다.
- 느리다!
#include <iostream>
#include <atomic>
#include <thread>
using namespace std;
int main()
{
atomic<int> shared_memory(0);
auto count_func = [&]()
{
for (int i = 0; i < 1000; ++i)
{
std::this_thread::sleep_for(std::chrono::microseconds(1));
shared_memory++; // 혹은 shared_memory.fetch_add(1);
}
};
std::thread t1 = std::thread(count_func);
std::thread t2 = std::thread(count_func);
t1.join();
t2.join();
cout << "After" << endl;
cout << shared_memory << '\n';
}
💎출력💎
After
2000
mutex 의 lock, unlock
#include <mutex>
shared_memroy++
을 A 스레드가 이미 작업 중이거나 작업을 하다가 말았으면 다른 스레드들이 못 건드리게 잠궈둔다.mutex mtx; mtx.lock(); shared_memory++; mtx.unlock();
- 단점
unlock
을 반드시 해줘야 다른 스레드가 작업을 수행할 수 있는데 프로그래머가 명시하는 것을 깜빡할 수도 있다.- 예외가 발생하거나 했을때
unlock
을 건너뛸 수도 있다.
#include <iostream>
#include <mutex>
#include <atomic>
#include <thread>
using namespace std;
int main()
{
int shared_memory(0);
mutex mtx;
auto count_func = [&]()
{
for (int i = 0; i < 1000; ++i)
{
mtx.lock();
shared_memory++;
mtx.unlock();
}
};
std::thread t1 = std::thread(count_func);
std::thread t2 = std::thread(count_func);
t1.join();
t2.join();
cout << "After" << endl;
cout << shared_memory << '\n';
}
💎출력💎
After
2000
std::lock_guard
#include <mutex>
mutex mtx;
std::lock_guard<mutex> lock(mtx); // unlock을 해주지 않아도 자동으로 해줌!
shared_memory++;
shared_memroy++
을 A 스레드가 이미 작업 중이거나 작업을 하다가 말았으면 다른 스레드들이 못 건드리게 잠궈둔다. mutex의lock 함수
,unlock 함수
와 기능은 동일하다.lock 함수
,unlock 함수
과 다른 점- 👉 스마트 포인터처럼 자신의 영역을 벗어나면 자동으로 unlock()을 호출해준다.
lock_guard
타입의 변수lock
은 for문 scope를 벗어나 메모리가 해제될 때 자동으로unlock
된다.
- 👉 스마트 포인터처럼 자신의 영역을 벗어나면 자동으로 unlock()을 호출해준다.
#include <iostream>
#include <mutex>
#include <atomic>
#include <thread>
using namespace std;
int main()
{
int shared_memory(0);
mutex mtx;
auto count_func = [&]()
{
for (int i = 0; i < 1000; ++i)
{
std::lock_guard<mutex> lock(mtx); // unlock을 해주지 않아도 자동으로 해줌!
shared_memory++;
}
};
std::thread t1 = std::thread(count_func);
std::thread t2 = std::thread(count_func);
t1.join();
t2.join();
cout << "After" << endl;
cout << shared_memory << '\n';
}
💎출력💎
After
2000
std::scoped_lock
#include <mutex>
mutex mtx;
std::scoped_lock<mutex> lock(mtx); // unlock을 해주지 않아도 자동으로 해줌!
shared_memory++;
shared_memroy++
을 A 스레드가 이미 작업 중이거나 작업을 하다가 말았으면 다른 스레드들이 못 건드리게 잠궈둔다. mutex의lock 함수
,unlock 함수
와 기능은 동일하다.std::lock_guard
함수와 동일하다. 스마트 포인터처럼 자신의 영역을 벗어나면 자동으로 unlock()을 호출해준다.- 단, C++ 17에서만 사용 가능하며
std::lock_guard
함수보다 권장된다.
#include <iostream>
#include <mutex>
#include <atomic>
#include <thread>
using namespace std;
int main()
{
int shared_memory(0);
mutex mtx;
auto count_func = [&]()
{
for (int i = 0; i < 1000; ++i)
{
std::scoped_lock<mutex> lock(mtx); // unlock을 해주지 않아도 자동으로 해줌!
shared_memory++;
}
};
std::thread t1 = std::thread(count_func);
std::thread t2 = std::thread(count_func);
t1.join();
t2.join();
cout << "After" << endl;
cout << shared_memory << '\n';
}
💎출력💎
After
2000
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글남기기