코가손의 블로그

[네트워크/C++] Lock을 얻기 위한 대기방법(1) SpinLock 본문

GameDev/NetworkProgramming

[네트워크/C++] Lock을 얻기 위한 대기방법(1) SpinLock

Cogason 2021. 12. 29. 19:24

Race Condition

두 개 이상의 스레드들이 공유 자원(*임계영역)에 접근하려고 할 때 동기화 매커니즘 없이 접근하려고 경쟁 하는 상황을 Race Condition이라고 한다.

 

Race Condition인 경우, 스레드의 실행 순서를 잘 조절해주지 않으면 이상한 상태, 비정상적인 결과가 발생할 수 있다.

이 문제는 항상 발생하는 것이 아니라 특정한 순서로 수행되었을 때만 발생하여 더욱 문제점을 찾기 어렵다.

 

그래서 멀티스레드 프로그래밍을 할 경우 특히 동기화 문제를 정말 잘 해결해야 한다.

 

*임계영역 : 공유되는 자원, 문제가 발생하지 않도록 독점을 보장해주어야 하는 영역

동시접근을 해결하기 위한 방법으로 lock, semaphore, monitor, 메시지를 이용한 기법 이 있다.

 

mutex와 lock

mutex는 Muteal exclusion(상호배제)의 약자로 임계영역 문제를 해결할 수 있는 개발 도구이다.

쉽게 이해하기 위해 자물쇠 또는 화장실 잠금장치 로 생각하면 좋다.

 

mutex(자물쇠)가 존재함으로 화장실을 열거나(unlock), 잠그는(lock)동작을 수행할 수 있다.

사람(쓰레드)이 화장실(임계영역)에 들어가서 자물쇠(mutex)를 잠그면(lock) 아무도(다른 쓰레드)들어갈 수 없고 다른 사람들은 대기한다. 자물쇠가 열리면(unlock)안에 있던 사람이 나오고, 다음 사람이 사용할 수 있게 된다.

 

쓰레드는 lock호출 이 성공한 시점부터 unlock을 호출 할 때까지 mutex를 소유한다.

 

 

"임계영역에 진입이 불가능 할 때, 락을 얻기 전에 대기하는 방법엔 무엇이 있을까?"

누군가가 화장실을 쓰고 있는 상황이라면 다음사람은 그 사람이 나올 때까지 기다리거나, 잠시 뒤에 오거나, 화장실 사용을 마쳤다는 알람 과 같은 장치를 사용해 화장실을 이용하는 방법이 있다.

순서대로 SpinLock, Sleep, Event 라고 한다.

SpinLock

스핀락은 임계영역에 진입이 불가능할 때 진입이 가능할 때까지 루프를 돌면서 기다리는 방식이다.

스핀 락은 busy wait의 한 종류이며 대기시간이 길어질수록 비효율적이다.

단, unlock을 계속 기다리고 있기 때문에 문맥교환 일어나지 않으며 lock을 얻고 해제하는 주기가 짧다면 SpinLock이 유용하게 적용될 수도 있다.

 

다음은 SpinLock에 대한 간단한 구현이다.

더보기
#include <iostream>
#include <thread>
#include <atomic>
#include <mutex>
using namespace std;

class SpinLock
{
public:

    bool expected = false; // lock이 걸리지 않은 상태를 기대함
    bool desired = true;

    void lock()
    {
        // CAS, lock이 걸리지 않은 상태(희망)라면 _locked를 desired상태로 바꿈
        while (_locked.compare_exchange_strong(expected, desired) == false)
        {
            expected = false;
        }
    }
    
    void unlock()
    {
        _locked.store(false);
    }
    
private:
    atomic<bool> _locked = false;
}

int sum = 0;
SpinLock spinLock;

void Add()
{
    for (int i = 0; i < 10'0000; ++i)
    {
        lock_guard<SpinLock> lock(spinLock);
        ++sum;
    }
}

void Sub()
{
    for (int i = 0; i < 10'0000; ++i)
    {
        lock_guard<SpinLock> lock(spinLock);
        --sum;
    }
}

int main()
{
    thread t1(Add);
    thread t2(Sub);
    
    t1.join();
    t2.join();
    
    cout << sum << endl;
    
    return 0;
}

 

Sleep

Sleep방식은 임계영역에 진입이 불가능할 때, 일정 시간 뒤에 다시 진입을 노리는 방법이다.

스핀락 과는 다르게 문맥교환의 비용이 든다.

더보기
    void lock()
    {
        // CAS, lock이 걸리지 않은 상태(희망)라면 _locked를 desired상태로 바꿈
        while (_locked.compare_exchange_strong(expected, desired) == false)
        {
            expected = false;
            this_thread::sleep_for(0ms);
            //this_thread::yield();
        }
    }

 

 

 

 

Comments