멀티쓰레드프로그래밍

SpinLock, Sleep, Event

kcj3054 2022. 4. 7. 02:43

SpinLock

  • SpinLockd은 lock이 있을 경우 자기 자리로 다시 돌아가는 것이 아닌 주위에서 계속해서 빙글빙글 돌아다닌 것을 의미한다.

spinlock 소스


#include "pch.h"
#include <iostream>

#include <thread>
#include <atomic>
#include <mutex>
using namespace std;

class SpinLock
{
public:
    void lock()
    {

        //CAS(Compare-And-Swap)

        bool expected = false; // 락을 걸기를 원하는 대상의 현재상태 그러나 현재는 false이다
        bool desired = true;  // 락이 걸리기를 기대하는 것

        ////CAS 의사코드 
        //if (expected == _locked)
        //{
        //   expected = _locked; // ???
        //   _locked = desired;  // 현재 lock을 true로 변경 
        //   return true;
        //}
        ////다른 누군가가 lock을 선점한 상태 !
        //else
        //{
        //   expected = _locked;
        //   return false;
        //}
        while (_locked.compare_exchange_strong(expected, desired) == false)
        {
            //expected가 &로 받아와서 항상 expected = false로 줘야한다
            expected = false;
        }
    }
    void unlock()
    {

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

int32 sum = 0;
mutex m;

void Add()
{
    for (int i = 0; i < 1'000'000; i++)
    {
        lock_guard<mutex> guard(m);
        sum++;
    }
}
void Sub()
{
    for (int i = 0; i < 1'000'000; i++)
    {
        lock_guard<mutex> guard(m);
        sum--;
    }
}
int main()
{
    volatile int32 a = 1;
    a = 2;
    a = 3;
    a = 4;

    thread t1(Add);
    thread t2(Sub);

    cout << sum;
}
  • 멀티스레드 환경에서는 공유 영역에있는 sum을 접근할때는 lock을 걸어 놓고 사용을 해야한다 그래서 Add, Sub에서 연산을 하기전에 lock을 걸어놓는다
//다른 누군가가 lock을 먼저 걸었다면 while안에서 뱅글뱅글 돈다. 
 while (_locked.compare_exchange_strong(expected, desired) == false)
        {
            //expected가 &로 받아와서 항상 expected = false로 줘야한다
            expected = false;
        }
  • 위의 atomic에는 compare_exchange_strong이 존재한다 앞에는 예상한 값, 뒤에는 기댓값이다 위의 소스에서 _locked.compare_exchange_strong(expected, desired) == falsed라면 계속해서 빙글빙글 while을 도는 것이 spinlock이다. 현재 다른 누군가가 lock을 선점한 상태라는 의미이다.
  • 락을 걸기 위해서 expected = false로 해 놓는 것은 해당 값이 락을 걸고 싶은거지 현재 lock이 걸려있는 것은 아니다 그래서 false.
  • 그리고 우리가 희망한 것은 lock이 걸리기릴 희망한 것라서 기댓값은 desired = true로 해놓는다.

spinLock 좋은건가? 나쁜건가?

  • spinLock은 양날의 검이다 왜?
    • spinLock이 길어지면 cpu 점유율이 높아진다(단점)
    • 그러나 금방 해제 될 lock이라면 제자리로 돌아갔다오면 context Switch가 발생해서 거기에 해당하는 비용이 더 들어가기에 대기하고 있다가 들어가는 것이 더 효율적이다.

Sleep 방법

  • sleep 방법은 spinlock 부분에서 약간만 수정만하면된다.

class SpinLock
{
public:
    void lock()
    {

        //CAS(Compare-And-Swap)

        bool expected = false; // 락을 걸기를 원하는 대상의 현재상태 그러나 현재는 false이다
        bool desired = true;  // 락이 걸리기를 기대하는 것

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

            //this_thread::sleep_for(std::chrono::microseconds(100));
            this_thread::sleep_for(100ms);
            this_thread::yield(); // yield는 sleep_for의 0ms랑 동일하다.!
        }
    }
    void unlock()
    {

    }
private:
    atomic<bool> _locked = false;
};
  • 위에서 while을 계속해서 돌게 하는 것이 아닌 --> !!! 진짜로 thread를 재우는 것이다. this_thread::sleep_for(100ms);

Event 방법

  • event는 멀티스레드에서 이야기하자면..'event개체는 커널에 있다. a라는 스레드가 event에게 요청을한다나 앞에 있는 스레드 unlock되면 나에게알려달라.. 이렇게 커널쪽에서 이벤트 개체가 나에게 알려주는 것이 event이다.
#include <iostream>
#include <vector>
#include <queue>
#include <Windows.h>
#include <thread>
#include <mutex>

using namespace std;

mutex m;
queue<int> q;
HANDLE handle;

void Produce()
{
    while (true)
    {
        {
            unique_lock<mutex> lock(m);
            q.push(100);
        }

        ::SetEvent(handle);

        this_thread::sleep_for(100ms);
    }
}
void Consumer()
{
    while (true)
    {
        ::WaitForSingleObject(handle, INFINITE);

        unique_lock<mutex> lock(m);

        while (q.empty() == false)
        {
            int data = q.front();
            q.pop();
            cout << data << endl;
        }

    }
}
int main()
{
    /*
    커널 오브젝트들은 공통적으로 
    - signal, non - signal 과
    - usage를 가지고 있다. 
    */

    handle = ::CreateEvent(0, false, false, NULL);

    thread t1(Produce);
    thread t2(Consumer);

    t1.join();
    t2.join();
    return 0;
}
  • 위 소스에서 이벤트가 없으면 무의미하게 while을 도는 경우가 생겨서 cpu점유율이 높아질 수 있는데 이벤트를 사용하면 사용 가능한 시점을 알 수 있어서 더욱 효율적이다.

결론

  • 루키스님의 서버 강의를 학습 후 작성하였습니다.