ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • C++ SpinLock구현
    C++/C++ 멀티스레드 2022. 10. 9. 13:44
    728x90

    SpinLock은 앞선 포스팅에서 말한 존버메타 lock이다.

    즉, lock을 다른 스레드가 사용중이면, 그 앞에서 lock이 풀릴때까지 계속 기다리는 것이다.

     

    이거를 한 번 구현해보자.

     

    • 들어가기에 앞서, 멀티스레드 프로그래밍을 할 때는, 내가 작성한 코드를 여러 스레드가 동시에 실행한다는 것을 잊지 말아야 한다. 그러지 않으면, 왜 에러가 나는지, 왜 코드가 내가 생각한대로 되지않는지 이해하기 어려워진다.

     

    #include <iostream>
    #include <thread>
    #include <atomic>
    #include <mutex>
    
    class SpinLock
    {
    public:
    	void lock()
    	{
    		while(_locked){		}
    		_locked = true;
    	}
    
    	void unlock()
    	{
    		_locked = false;
    	}
    
    private:
    	bool _locked = false;
    };
    
    int sum = 0;
    SpinLock spinLock;
    
    void Add()
    {
    	for (int i = 0; i < 10000; i++)
    	{
    		lock_guard<SpinLock> guard(spinLock);
    		sum++;
    	}
    }
    
    void Sub()
    {
    	for (int i = 0; i < 10000; i++)
    	{
    		lock_guard<SpinLock> guard(spinLock);
    		sum--;
    	}
    }
    
    void main()
    {
    	std::thread t1(Add);
    	std::thread t2(Sub);
    
    	t1.join();
    	t2.join();
    
    	cout << sum << endl;
    }

     

    가장 기본적으로 생각해볼 수 있는 방식이다.

     

    어떤 스레드가 _locked를 사용한다면 _locked는 true가 되고,

    다른 스레드는 while(_locked)에서 막혀서 대기하는 상황인 것이다.

    그리고, lock을 사용하던 스레드가 unlock을 통해서 _locked를 false로 바꿔주면,

    while문에서 기다리던 스레드들이 lock을 가질수 있는 아주 논리적이고 이성적인 방법으로 보인다.

     

    하지만 실행해보면 시궁창이다

     

    문제점을 분석해보자.

    • 프로그램이 시작된다.
    • 스레드 t1과 t2가 lock을 얻기 위해서 달려간다.
    • 최초 _locked는 false이므로, 먼저 도착하는 스레드는 while문에서 막히지 않고 통과할 수 있을 것 같다.
    • 하지만, 두 개의 스레드가 동시에 while문에 도착할 수도 있다.
    • 두 개의 스레드 모두, _locked=true를 실행하기 전이라면, 모두 while문을 통과해서  _locked=true를 실행할 수 있다.
    • 그러면 두 스레드 모두 lock을 얻게 된다.

     

    어떻게 해결해야 할까?

    저번에 알아봤던 atomic에서 아이디어를 얻을 수 있다.

     

    while(_locked)를 검사하는 부분과

    _locked=true를 실행하는 부분을 한 번에 실행해야 하면 된다.

     

    이것과 관련된 함수는 atomic변수를 사용하면 제공된다.

    _locked를 atomic변수로 선언하고, compare_exchange_strong함수를 사용하면 된다.

    일단 코드부터 보고, 분석해보자.

     

    class SpinLock
    {
    public:
    	bool expected = false;
    	bool desired = true;
    
    	void lock()
    	{
    		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 < 10000; i++)
    	{
    		lock_guard<SpinLock> guard(spinLock);
    		sum++;
    	}
    }
    
    void Sub()
    {
    	for (int i = 0; i < 10000; i++)
    	{
    		lock_guard<SpinLock> guard(spinLock);
    		sum--;
    	}
    }

    이렇게 수정해주면 되는데

     

    _locked.compare_exchange_strong함수를 보자.

    이 함수가 하는 일은 다음과 같다.

     

    • _locked==expected라면, _locked=desired를 수행하고, true를 반환한다.
    • _locked!=expected라면, false를 반환한다.

     

    우리는 다른 스레드가 lock을 갖고 있는 동안은 while문을 계속 돌며 대기하길 바란다.

    따라서 우리가 기대하는 값(expected)은 lock을 아무도 갖고있지 않은 false인 상황이고,

    lock을 아무도 갖고 있지 않다면, 그 lock을 desired값. 즉, true로 설정하여 lock을 갖게 되는 것이다.

     

    이 함수를 의사코드로 작성해보면 다음과 같다.

        //lock을 가질 수 있는 상황
        if (_locked == expected)
        {
            expected = _locked;
            _locked = desired;
            return true;
        }
        //다른 스레드가 lock을 가지고 있는 상황.
        else
        {
            expected = _locked;
            return false;
        }

     

    (나도 배운 코드인데, 사실 expected = _locked를 수행하는 이유는 모르겠다. 이 라인이 없다면,

    while문을 돌며, expected=false를 계속 해줄 필요도 없을텐데)

     

    여튼.. 이렇게 spinlock을 구현하면, 기존의 C++에서 제공하는 mutex를 사용하는 것과 동일하게 lock이 잘 작동한다.

    728x90

    댓글

Designed by Tistory.