졸업 작품/서버

C++ 스핀락 구현

장형이 2019. 6. 13. 23:14

학생이라 아직 잘 모릅니다.. 틀린 내용이 있으면 알려주시면 감사하겠습니다. ㅠㅜ

 

동기화를 맞추는 기법이 여러 개 있는데 그중 하나인 스핀 락에 대해서 한번 알아보고 기록해보았다.

 

1. 스핀락이란?

 

스핀 락은 mutex처럼 시스템 콜을 하여 락을 얻지 못하면 쉬는 방법이 아닌, 락을 걸지 못하면 무한 루프를 돌면서 계속 얻으려고 시도하는 동기화 기법이다.

간단한 그림

 

 

SpinLock은 불가능하면 다시 시도하고... 를 반복한다. 즉 쉬지 않기 때문에 바쁜 대기(busy wating)라고도 부른다.

 

바쁜 대기의 장점은 따로 시스템 콜이 들어가지 않아서 lock을 얻고 해제할 때의 비용이 매우 작다는 장점이 있다. 또한 자신의 시간을 다른 스레드에게 넘기지 않아 콘텍스트 스위칭이 잘 일어나지 않는다는 장점이 있다.

 

하지만 단점으로 lock을 걸고 하는 일이 많아 다른 스레드에서 대기가 길어질 때, 기다리는 데에 자원을 사용하므로 쓸데없는 시간 낭비가 엄청나게 생길 수 있다는 단점이 있다.

 

정리하면 자주 Lock이 일어나지 않고, 짧은 시간에 끝나면 효율적,

Lock이 매우 빈번하고 금방 Unlock이 되지 않는다면 비효율적이라고 할 수 있다.

 

2. 스핀 락 구현

 

스핀락 구현은 위 그림과 같이 매우 간단하다.

Lock이 아니면 Lock을 걸고,

Lock 중이라면 Lock을 시도하기만 하면 된다.

 

#include <mutex>
class CSpinlock {
public:
	void Lock()
	{
		while (lock.test_and_set(std::memory_order_acquire)) {

		}
	}
	bool TryLock()
	{
		return !lock.test_and_set(std::memory_order_acquire);
	}
	void Unlock()
	{
		lock.clear(std::memory_order_release);
	}

private:
	std::atomic_flag lock = ATOMIC_FLAG_INIT;
};

 

atomic은 원자적이라는 뜻이다.

좀 더 이해하기 쉽게 풀어서 말하면 한 번에 하나의 행동이 되는 것을 보장한다는 뜻이다.

 

atomic_flag 타입은 원자적인 수정 함수를 제공하는 타입이다.

test_and_set 함수와 clear 함수의 자세한 역할은 여기(https://en.cppreference.com/w/cpp/atomic/atomic_flag)를 참고하면 된다. 

 

3. 스핀 락 테스트

 

스핀 락을 테스트하기 위해서 간단한 프로젝트를 만들었다.

 

// ConsoleApplication11.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include <iostream>
#include <thread>
#include <mutex>
#include "SpinLock.h"

std::mutex mt;
CSpinlock locker;

#define ITER_TIME 10000000
#define THREAD_COUNT 1
#define TRY_TIME 6

bool isUseMutex;
int counter;
void Counter() {
	for (int i = 0; i < ITER_TIME; ++i) {
		if(isUseMutex) mt.lock();
		else locker.Lock();

		// 카운터를 1 더해줍니다.
		counter++;

		if(isUseMutex) mt.unlock();
		else locker.Unlock();
	}
}
#include <stdio.h>
#include <time.h>
#include <windows.h>
int main()
{
	double spinlockTime = 0.f;
	double mutexTime = 0.f;

	for (int k = 0; k < TRY_TIME; ++k) {
		clock_t start, end;
		for (int i = 0; i <= 1; ++i) {
			// 먼저 counter를 초기화 합니다.
			counter = 0;
			isUseMutex = (bool)i;

			start = clock();

			// 쓰레드를 가동시킵니다.
			std::thread** counterThreads;
			counterThreads = new std::thread * [THREAD_COUNT];
			for (int j = 0; j < THREAD_COUNT; ++j) {
				counterThreads[j] = new std::thread(Counter);
			}
			for (int j = 0; j < THREAD_COUNT; ++j) {
				counterThreads[j]->join();
			}
			end = clock();
			delete[] counterThreads;

			double result = (double)(end - start) / CLOCKS_PER_SEC;

			if (isUseMutex) mutexTime += result;
			else spinlockTime += result;
		}
	}

	printf("Thread : %d, Iterator Time : %d\n", THREAD_COUNT, ITER_TIME);
	printf("Mutex : %f\n", mutexTime / TRY_TIME);
	printf("Spin : %f\n", spinlockTime / TRY_TIME);

	return 0;
}

 

수행 결과는 다음과 같았다.

 

결과를 보니 확실히 Lock 자체로의 능력은 별로 좋지 않지만,

Lock으로 인해 대기가 없을수록 매우 좋은 효율을 낸다는 것을 알 수 있었다.

 

참고로 나의 컴퓨터는 4 스레드까지 지원한다고 하니 실험이 의미가 없을지도....

spinlock 관련 효율 체크는 여기(https://smallake.kr/?p=6356)가 더 정확할 것 같다.