for (int i = 0; i < 10000; i++) {
for (int j = 0; j < 10000; j++) {
s += data[j];
}
}
<2번 코드>for (int j = 0; j < 10000; j++) { //j가 밖에
for (int i = 0; i < 10000; i++) {
s += data[j];
}
}
한 작업이 끝나기 전에, 다음 작업을 시작하는 방식으로 동시에 여러개의 작업을 동시에 실행하는 것
CPU에서 명령어를 실행할 때 여러 단계를 거침
위 단계의 실행 속도는 명령어마다 천차 만별임
컴파일러는 명령어를 재배치할 때, 다른 쓰레드들을 고려하지 않아 멀티 쓰레드 환경에서 예상치 못한 결과가 나올 수 있음
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
void worker(std::atomic<int>& counter) {
for (int i = 0; i < 10000; i++) {
counter++;
}
}
int main() {
std::atomic<int> counter(0);
std::vector<std::thread> workers;
for (int i = 0; i < 4; i++) {
workers.push_back(std::thread(worker, ref(counter)));
}
for (int i = 0; i < 4; i++) {
workers[i].join();
}
std::cout << "Counter 최종 값 : " << counter << std::endl;
}
Counter 최종 값 : 40000
std::atomic<int> counter(0);
atomic
의 템플릿 인자로 원자적으로 만들고 싶은 타입을 전달counter++
이 뮤텍스의 보호를 받지 않아도 수행 중 다른 쓰레드가 실행되지 않음atomic
객체의 연산들이 정말 원자적으로 구현 가능한지 확인하기 위해선 is_lock_free()
함수를 호출하면 됨std::atomic<int> x;
std::cout << "is lock free ? : " << boolalpha << x.is_lock_free() << std::endl;
실행 결과: Is lock free ? : true
atomic
객체들의 경우 원자적 연산 시에 메모리에 접근할 때 어떤 방식으로 접근 하는지 지정 가능#include <atomic>
#include <cstdio>
#include <thread>
#include <vector>
using std::memory_order_relaxed;
void t1(std::atomic<int>* a, std::atomic<int>* b) {
b->store(1, memory_order_relaxed); // b = 1 (쓰기)
int x = a->load(memory_order_relaxed); // x = a (읽기)
printf("x : %d \n", x);
}
void t2(std::atomic<int>* a, std::atomic<int>* b) {
a->store(1, memory_order_relaxed); // a = 1 (쓰기)
int y = b->load(memory_order_relaxed); // y = b (읽기)
printf("y : %d \n", y);
}
int main() {
std::vector<std::thread> threads;
std::atomic<int> a(0);
std::atomic<int> b(0);
threads.push_back(std::thread(t1, &a, &b));
threads.push_back(std::thread(t2, &a, &b));
for (int i = 0; i < 2; i++) {
threads[i].join();
}
}
x : 1
y : 0
x : 0
y : 1
x : 1
y : 1
memory_order_relaxed
방식으로 실행b->store(1, memory_order_relaxed); // b = 1 (쓰기)
int x = a->load(memory_order_relaxed); // x = a (읽기)
memory_order_acquire
으로 읽는 쓰레드가 있다면, memory_order_release
이전에 오는 모든 메모리 명령들이 해당 쓰레드에 의해서 관찰될 수 있어야 함예제 코드
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
using std::memory_order_relaxed;
std::atomic<bool> is_ready;
std::atomic<int> data[3];
void producer() {
data[0].store(1, memory_order_relaxed);
data[1].store(2, memory_order_relaxed);
data[2].store(3, memory_order_relaxed);
is_ready.store(true, std::memory_order_release);
}
void consumer() {
// data 가 준비될 때 까지 기다린다.
while (!is_ready.load(std::memory_order_acquire)) {
}
std::cout << "data[0] : " << data[0].load(memory_order_relaxed) << std::endl;
std::cout << "data[1] : " << data[1].load(memory_order_relaxed) << std::endl;
std::cout << "data[2] : " << data[2].load(memory_order_relaxed) << std::endl;
}
int main() {
std::vector<std::thread> threads;
threads.push_back(std::thread(producer));
threads.push_back(std::thread(consumer));
for (int i = 0; i < 2; i++) {
threads[i].join();
}
}
실행결과
data[0] : 1
data[1] : 2
data[2] : 3
data
의 원소들은 store
하는 명령들은 모두 relaxed
덕분에 자기들 끼리 재배치 가능하지만, 아래 release
이후로 넘어가서 재배치 될 순 없음consumer
에서 data
들의 값을 확인했을 때 언제나 적확하게 1,2,3이 들어있게 됨data[0].store(1, memory_order_relaxed);
data[1].store(2, memory_order_relaxed);
data[2].store(3, memory_order_relaxed);
is_ready.store(true, std::memory_order_release);
acquire
와 release
를 모두 수행하는 것