이번 시간에는 C++의 atomic, memory order 등에 관해서 알아보겠습니다.

std::atomic

스레드간 동기화 문제가 발생할 때 해결하는 방법

  1. OS 제공 동기화 도구 사용 std::mutex
  2. CPU가 제공하는 OPCODE 사용 (인텔의 경우 lock prefix 사용, atomic operation)
#include <iostream>
#include <thread>
#include <mutex>
#include <windows.h>

std::mutex m;
long x = 0; // 모든 스레드가 공유.

void foo()
{
    for ( int i = 0; i < 100000; ++i)
    {
//        m.lock();
//        ++x;
//        m.unlock();

//        __asm
//        {
//            lock inc x
//        }
        InterlockedIncrement(&x);
    }
}

int main()
{
    std::thread t1(foo);
    std::thread t2(foo);
    std::thread t3(foo);
    t1.join();
    t2.join();
    t3.join();
    std::cout << x << std::endl;
}

C++ 표준에서 제공하는 원자 연산 방법

#include <iostream>
#include <thread>
#include <atomic>

std::atomic<long> x{0};

void foo()
{
    for ( int i = 0; i < 100000; ++i)
    {
        // atomic op flag
        ++x; // x.operator++()
    }
}

int main()
{
    std::thread t1(foo);
    std::thread t2(foo);
    std::thread t3(foo);
    t1.join();
    t2.join();
    t3.join();
    std::cout << x << std::endl;
}

std::atomic 연산

구 분 내 용
연산자 재정의 ++, –, +=, -=, &=, ^=, |=
멤버 함수 fetch_add, fetch_sub, fetch_and, fetch_or, fetch_xor, is_lock_free, load, store
  • fetch_ 함수들은 memory_order를 인자로 전달 할 수 있음

lock-free

  • OS 수준 동기화 도구를 사용하지 않고, CPU level의 명령어를 사용해서 동기화 하는 것

memory order

Reordering

  • 성능 향상을 위해서 어셈블리 레벨에서 코드의 실행훈서를 변경하는 것
  • 컴파일러 최적화에 의해 하단 코드가 의도한대로 동작하지 않을 수 있음(컴파일 시간 or 런타임 모두 발생)
#include <atomic>
int a = 0;
int b = 0;

// thread A
void foo()
{
    a = b + 1;
    //----- fence -----------
    //__asm { mfence }
    //std::atomic_thread_fence( std::memory_order_release); // reordering을 막는 방법
    b = 1;
}

// thread B
void goo()
{
    if ( b == 1 )
    {
        // a == 1 을 보장할수 있을까 ?
    }
}

memory order 옵션

  • 동기화 수준
구 분 내 용
memory_order_relaxed 오버헤드 가장 적음, atomic operation만 보장, 실행 순서 변경 가능
memory_order_consume
memory_order_acquire
memory_order_release
memory_order_acq_rel
memory_order_cst
#include <atomic>
#include <thread>

std::atomic<int> x = 0;
std::atomic<int> y = 0;

void foo()
{
    int n1 = y.load(std::memory_order_relaxed);
   	x.store(n1, std::memory_order_relaxed);
}

void goo()
{
    int n2 = x.load(std::memory_order_relaxed);
    y.store(100, std::memory_order_relaxed);
}

void main()
{
    std::thread t1(foo);
    std::thread t2(goo);
}

release-acqure 모델

  • release 이전의 코드는 acquire 이후에 읽을 수 있다는 것을 보장
#include <atomic>
#include <thread>

std::atomic<int> data1 = 0;
std::atomic<int> data2 = 0;
std::atomic<int> flag = 0;

void foo()
{
    data1.store(100, std::memory_order_relaxed);
    data2.store(200, std::memory_order_relaxed);
   	flag.store(1, std::memory_order_release);
}

void goo()
{
	if (flag.load(std::memory_order_acquire) > 0)
    {
       assert(data2.load(std::memory_order_relaxed) == 10);
       assert(data2.load(std::memory_order_relaxed) == 20);
    }
}

void main()
{
    std::thread t1(foo);
    std::thread t2(goo);
}