[C++ 서버] SpinLock

이정석·2023년 11월 6일

CppServer

목록 보기
3/8

SpinLock

SpinLock에 대한 설명은 여기서확인할 수 있다.

C++에서 SpinLock을 구현하기 위해서 어떻게 해야할까? 우선, SpinLock대신 mutex를 사용하는 아래 코드를 보자.

  int32 sum = 0;
  mutex m;

  void Add() {
      for (int32 i = 0; i < 100000; i++) {
          lock_guard<mutex> guard(m);
          sum++;
      }
  }

  void Sub() {
      for (int32 i = 0; i < 100000; i++) {
          lock_guard<mutex> guard(m);
          sum--;
      }
  }

  int main() {
      thread t1(Add);
      thread t2(Sub);

      t1.join();
      t2.join();

      cout << sum << endl;

      return 0;
  }

각 쓰레드는 공유 변수인 sum에 접근하기 전에 lock_guard<mutex>를 이용해 Lock을 얻고 sum에 대한 조작을 한다. 위 코드의 결과는 0이 출력된다.

SpinLock을 구현하고 위 코드의 lock_guard<mutex>대신에 lock_guard<SpinLock>을 사용하고 마찬가지로 0이 출력된다면 SpinLock이 잘 구현됐다고 할 수 있다.

1. SpinLock Class

먼저 가장 간단한 방법으로 SpinLock을 구현하면 아래 코드와 같이 작성할 수 있다. 다른 쓰레드가 Lock을 얻었는지 bool변수로 확인하고 Lock을 얻은 쓰레드가 없다면 bool변수를 true로 해주어 다른 쓰레드에게 자신이 Lock을 얻었음을 알리는 방식을 사용할 수 있다.

  class SpinLock {
  public:
      void lock() {
          while (_locked) {

          }

          _locked = true;
      }

      void unlock() {
          _locked = false;
      }

  private:
      bool _locked = false;
  };

SpinLock의 인터페이스는 lockunlock이 있어야 한다. 그 이유는 아래 사진을 보면 알 수 있는데 실제 lock_guard의 내부 구현이다.

구현부분을 보면 _Mutex로 들어온 객체에 대한 lock, unlock 연산을 호출해주기 때문에 mutex대신에 들어갈 SpinLock 클래스는 lockunlock을 지원해야 한다.

하지만, 위의 코드대로 구현하고 실행한다면 0이 출력되지 않을 것이다. 그 이유는 bool변수의 확인변경이 분리되어 있기 때문이다.

2. Compare And Swap

'확인과 변경이 분리되어 있다.'는 말은 어떤 의미일까? 이 상황을 이해하기 위해서 위 코드가 왜 0이 출력되지 않는지 알아야 할 필요가 있다.

  1. 쓰레드 t1이 Lock을 얻기 위해 시도한다.
  2. 쓰레드 t2가 Lock을 얻기 위해 시도한다.
  3. t1이 _locked가 false임을 확인하였다.
  4. t1이 _locked를 true로 변경하기 전에, t2는 _locked가 false임을 확인하였다.
  5. t1, t2가 동시에 sum에 접근한다.

위와 같이 확인과 변경이 동시에 이루어지지 않으면 Lock을 보장할 수 없다. 이를 위해 사용할 수 있는 것은 Interlock계열 함수와 Atomic이다.

Interlocked함수는 Window 운영체제에서 사용되는 함수로 원자적으로 변수의 값에 대한 조작을 할 수 있다. 즉, 중간에 다른 쓰레드에 의해 방해받지 않고 수행됨을 보장할 수 있다.

3. Atomic

atomic은 여러 원자적 연산을 지원하는데 그 중에서 exchange함수를 사용하면 쉽게 Compare And Swap을 사용할 수 있다.

exchange는 atomic변수를 변경하고 이전의 값을 반환하는 함수이지만, SpinLock을 구현하기에는 비교 과정이 없다. 이를 위해 compare_exchange_strong를 이용하면 SpinLock을 구현할 수 있다.

  class SpinLock {
  public:
      void lock() {

          bool expected = false;
          bool desired = true;

          while (_locked.compare_exchange_strong(expected, desired) == false) {
              expected = false;
          }
      }

      void unlock() {
          _locked.store(false);
      }

  private:
      atomic<bool> _locked = false;
  };

'compare_exchange_strong'expected, desired를 매개변수로 받는데 각 매개변수는 다음과 같은 의미를 가지고 있다.

  • expected: atomic 값과 비교될 값
  • desired: 비교 결과가 참일 경우 변경될 값

즉, 위 코드의 compare_exchange_strong는 아래 과정을 한번에 처리한다.

if (_locked == expected) {
	expected = _locked;
	_locked = desired;
    return true;
}
else {
	expected = _locked;
    return false;
}

expected와의 비교와 desired와의 변경 과정을 한번에 해주는데 compare_exchange_strong의 반환 값은 atomic 값과 expect와의 일치 여부이다.

expect는 참조형식으로 들어가기 때문에 비교가 실패할 경우 expect의 값이 바뀔 수 있기 때문에 while문 안에 expect를 초기화 해주는 부분을 추가해주어야 한다.

위의 코드대로 실행한다면 출력값은 0이 될 것이다. SpinLock은 다른 쓰레드로 Context Switch가 되기 전에 Lock에 대한 접근을 시도하기 때문에 비교적 짧은 시간 내에 Unlock되는 작업에 사용하면 좋다. 만약, 시간이 오래 걸리는 작업에 대해서 SpinLock을 사용하면 CPU 자원의 낭비로 이어질 것이다.

profile
게임 개발자가 되고 싶은 한 소?년

0개의 댓글