ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • C++, Sleep을 이용한 Lock구현
    C++/C++ 멀티스레드 2022. 10. 10. 11:52
    728x90

    이전 포스팅에서는 spinlock을 구현해봤다.

    spinlock은 lock을 얻기위해서, 기한없는 대기를 하는 방법이다.

    https://bethetitan.tistory.com/9

     

    C++ SpinLock구현

    SpinLock은 앞선 포스팅에서 말한 존버메타 lock이다. 즉, lock을 다른 스레드가 사용중이면, 그 앞에서 lock이 풀릴때까지 계속 기다리는 것이다. 이거를 한 번 구현해보자. 들어가기에 앞서, 멀티스

    bethetitan.tistory.com

     

    spinlock을 이용할 경우, CPU를 더 효율적인 곳에 사용하지 못하고, 가만히 놀게 하는 것과 다름없으므로,

    lock을 가진, thread가 lock을 빨리 해제하지 않는 경우에는 비효율적이라는 단점이 있다.

     

    그래서 오늘은 lock이 잠겨있다면, lock을 얻으려는 thread가 랜덤한 시간만큼 대기하다가

    그 시간이 지난 후에 다시 lock을 얻을 수 있는지 알아보는 방법을 구현해보겠다.

     

    그 전에 몇 가지 배경지식이 필요하다.

     

    OS-Job Scheduling

    운영체제 시간에 배우는 건데,

    우리 컴퓨터에서 가장 핵심적인 역할을 하는 것은 CPU이다.

    가장 빠르고, 많은 일을 할 수 있다.

     

    따라서 이 CPU를 쉬지 않게하고, 적재적소에 배치해야 컴퓨터의 성능을 향상시킬 수 있는 것이다.

     

    이렇게 CPU를 적재적소에 배치하는 방법을 Job Scheduling이라고 한다.

     

    Job Scheduling방법에는 다양한 알고리즘이 있는데

    FCFS - 먼저온 작업을 먼저 처리

    Priority - 우선순위에 따라 처리

    Round Robin - 공평하게 시간을 분배하여 처리

    등등 이것 말고도 많다.

     

    현대 컴퓨팅에서는 RoundRobin 방식을 많이 이용한다.

    RoundRobin방식은 Time Slice만큼, 한 프로세스(or 스레드)가 실행되고,

    그 다음 프로세스가 또 Time Slice만큼 실행되고... 이런 방식이다.

     

    5개의 프로세스가 있고, Time Slice가 1초라면

    각 프로세스가 돌아가면서 1초씩 CPU에 대한 이용권을 얻어서

    작업을 수행하는 것이다.

     

    그래서?

    그래서... spinlock방식을 이용하게 되면, CPU이용권을 얻은 thread는 time slice 내내 while문 앞에서 줄만 서있다가 다음 스레드에게 CPU이용권을 넘겨주게 될 수도 있다.

     

    하지만 랜덤한 시간만큼 잠시 쉬다가 오는 방법을 이용한다면

    내 time slice를 다 쓰진 않았는데,,, 나 어차피 줄만 서다가 끝날거 같으니까 너 먼저 써!

    하고 다른 thread에게 양보를 하는 것이다.

     

    물론 효율적인 방법일 수 있지만, 단점도 있다.

    • 내가 양보하자 마자 자리가 "날수도" 있다.
    • 컨텍스트 스위칭이 발생한다.
      • thread가 lock을 얻는 행위는 '유저모드'에서 일어난다. 하지만, thread가 lock을 얻지 못해서, 다른 thread가 실행되도록 하려면, '커널모드'로 전환해서, 잡 스케줄러가 다음 thread에게 CPU이용권을 넘겨줘야 한다.
      • 이 때 실행하던 thread와 관련된 정보를 레지스터에서 메모리로 옮기고, 실행하려는 thread와 관련된 정보를 메모리에서 레지스터로 가져오는 것을 Context Switching이라하고, 이게 시간이 좀 걸린다.

     

    어떻게 구현하는가?

    정말 쉽다. 저번에 구현했던 spinlock에 코드 1줄만 추가하면 된다.

     

    #include <thread>
    #include <atomic>
    #include <mutex>
    
    class SpinLock
    {
    public:
    	void lock()
    	{
    		bool expected = false;
    		bool desired = true;
    		while (_locked.compare_exchange_strong(expected, desired) == false)
    		{
    			cout << this_thread::get_id << "자고 올게" << endl;
    			expected = false;
    			this_thread::sleep_for(100ms);
    			//this_thread::sleep_for(std::chrono::milliseconds(100));
    			//this_thread::yield();
    		}
    	}
    
    	void unlock()
    	{
    		_locked.store(false);
    	}
    private:
    	atomic<bool> _locked=false;
    };
    
    int sum = 0;
    SpinLock spinLock;
    
    void Add()
    {
    	for (int i = 0; i < 10000000; i++)
    	{
    		lock_guard<SpinLock> guard(spinLock);
    		sum++;
    	}
    }
    
    void Sub()
    {
    	for (int i = 0; i < 10000000; i++)
    	{
    		lock_guard<SpinLock> guard(spinLock);
    		sum--;
    	}
    }
    
    void main()
    {
    	std::thread t1(Add);
    	std::thread t2(Sub);
    
    	t1.join();
    	t2.join();
    
    	cout << sum << endl;
    }

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

    의 조건문을 만족했다는 것은, lock을 얻지 못했다는 뜻이다.

     

    그러면 잡 스케줄러에게 나는 잠시 쉬다가 올테니, CPU이용권을 다른 스레드에게 넘겨줘!

    라고 명령을 넘겨줘야한다.

     

    그러한 명령은 sleep혹은 yield를 통해서 가능하다.

     

    • sleep_for(time) : time 동안은, 혹여나 내 차례가 다시 돌아온다 하더라도 CPU의 이용권을 얻지 않겠습니다!
    • yeild() : 일단 양보는 하는데, 내 차례가 돌아오면 언제든 CPU의 이용권을 얻겠습니다!

    이런 차이가 있다.

     

    위의 코드에서 sleep_for를 넣었을 때와, 넣지 않았을 때를 비교하며 실행해보자.

    sleep_for를 넣으면 "자고올게!"라는 출력이 한 번만 나올 것이고

    sleep_for를 했을 때

    sleep_for를 넣지 않으면, while문에서 기다리는 동안 출력이 여러번 나올 것이다.

    sleep_for를 하지 않았을 때

     

    참고

    유저모드와 커널모드의 전환을 야기하는 시스템콜은 시간이 꽤나 걸린다.

    모니터에 출력을 하는 cout의 경우

    유저모드에서 커널모드로 전환 후, 출력을 하고, 다시 유저모드로 돌아와서 하던 일을 이어서 해야하기 때문에 그렇다.

     

    그러니... 성능이 중요한 시스템에서는 유저모드와 커널모드를 왔다갔다 하는 I/O Interrupt를 최소화 하는 것이 좋다.

    728x90

    댓글

Designed by Tistory.