ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • C++, 멀티스레드 교착상태(DeadLock)
    C++/C++ 멀티스레드 2022. 10. 8. 11:06
    728x90

    DeadLock은 멀티스레드 프로그래밍을 할 때 만나게 되는 대표적인 문제점 중 하나이다. 

     

    DeadLock의 개념부터 알아보고 넘어가자.

    • 각각의 스레드가 Lock을 갖고 있는데, Lock을 놓기 위해서는 다른 스레드의 Lock이 필요한 상황

    을 DeadLock이라고 할 수 있을 것 같다.

     

    사실 잘 와닿지 않을 수 있다.

     

    적절한 예시가 될지 모르겠지만 데드락을 배우다보니 이런 상황이 생각이 났다.

     

    고등학교를 다닐 때 우리 반 선생님은 엄한 사람이였다.

    체벌이 있던 시절이다 보니, 야간자율학습을 하지 않고 도망가면 다음날에 적어도 3대씩은 맞았다.

    기분에 따라서 5대까지 늘어나기도 했다.

     

    하지만 나는 야간자율학습을 성실히 하던 학생이 아니였다.

    항상 저녁을 먹고 학교에 올라와서 친구들과 도망갈까말까에 대한 고민을 했다.

     

    어떤 날은 다들 뜻이 맞아서 

    "갈까?? 가자!! 그래 가자!!!" 하고 가는 날도 있었지만

     

    어떤 날은 후환이 두려워서 책임을 전가할 때가 있었다.

    A : B가면 나도 갈게

    B : C가면 나도 갈게

    C : A가면 나도 갈게

     

    이게 데드락이다.

     

    각설하고, C++에서 데드락 상태를 정의해보자.

    어떤 게임이 있다고하자

    두 개의 문이 있는데, 두 개의 문의 Lock(자물쇠)를 모두 얻으면 되는 간단한 게임이다.

    데드락 상태를 모델링 하기 위해서 코드를 조금 억지로 만들어 봤다.

     

    Door1에서 GameStart를 하면, Door1의 Lock을 획득하고,

    이어서 Door2의 Lock을 획득하게 된다.

     

    Door2에서는 그 반대다.

     

    Door1.h

    #pragma once
    #include <mutex>
    
    class Door1
    {
    public:
    	static Door1* Instance()
    	{
    		static Door1 singletone;
    		return &singletone;
    	}
    
    	void getLockA()
    	{
    		lock_guard<mutex> guard(_LockA);
    	}
    
    	void GameStart();
    
    private:
    	mutex _LockA;
    };

    Door1.cpp

    #include "pch.h"
    #include "Door2.h"
    #include "Door1.h"
    
    void Door2::GameStart()
    {
    	//get Lock B
    	lock_guard<mutex> guard(_LockB);
    
    	//get Lock A
    	Door1::Instance()->getLockA();
    }

    Door2.h

    #pragma once
    #include <mutex>
    
    class Door2
    {
    public:
    	static Door2* Instance()
    	{
    		static Door2 singletone;
    		return &singletone;
    	}
    
    	void getLockB()
    	{
    		lock_guard<mutex> guard(_LockB);
    	}
    
    	void GameStart();
    
    private:
    	mutex _LockB;
    };

    Door2.cpp

    #include "pch.h"
    #include "Door2.h"
    #include "Door1.h"
    
    void Door2::GameStart()
    {
    	//get Lock B
    	lock_guard<mutex> guard(_LockB);
    
    	//get Lock A
    	Door1::Instance()->getLockA();
    }

    그리고 main에서 두 개의 스레드를 생성하여, 게임을 약 10번씩 수행해보자.

    #include <iostream>
    #include <thread>
    #include <mutex>
    #include "Door1.h"
    #include "Door2.h"
    
    void Func1()
    {
    	for (int i = 0; i < 10; i++)
    	{
    		Door1::Instance()->GameStart();
    	}
    }
    
    void Func2()
    {
    	for (int i = 0; i < 10; i++)
    	{
    		Door2::Instance()->GameStart();
    	}
    }
    
    int main()
    {
    	std::thread t1(Func1);
    	std::thread t2(Func2);
    
    	t1.join();
    	t2.join();
    
    	cout << "Job Done" << endl;
    }

     

    위의 코드를 실행하면 어떨 때는 잘 실행되고, 어떨때는 프로그램이 멎어버린다.

     

    이건 데드락의 특징 중 하나인데, 항상 에러가 발생하지는 않는다는 것이다.

     

    그렇다 보니 개발단계에서 수없이 테스트를 할 때는 멀쩡하게 돌아가다가도,

    시스템을 실제로 배포하고 다수의 사용자가 사용하기 시작하면 데드락이 발생해버리는 일이 꽤나 잦다.

     

    위의 경우에서 데드락이 발생하는 원인은 다음과 같다.

     

    t1이 Door1의 GameStart를 해서 LockA를 갖는다.

    t2가 Door2의 GameStart를 해서 LockB를 갖는다.

    t2가 GameStart 함수를 끝내기 위해서는 Door1의 LockA가 필요하다.

    t1이 GameStart 함수를 끝내기 위해서는 Door2의 LockB가 필요하다.

     

    이렇게 각 스레드가 가진 lock을 해제하지 않는 상황에서

    상대 스레드가 가진 lock을 원하는 상황이 발생한 것이다.

    대충 이런 상황인 거다.

     

    데드락의 해결방법은?

    애석하게도 데드락을 100% 해결하는 방법이 없다.

     

    하지만, 교착상태를 줄이거나, 발생했을 때 알아차리는 방법은 몇가지가 있다.

     

    그럼 각 방법들에 대해서 살펴보자

    1. 우선순위 부여

    - lock에 우선순위를 부여해서 항상 우선순위대로 실행한다.

    - 하지만, lock이 많을 경우 모든 lock에 대해서 우선순위를 부여하는 것은 아주 소모적이다.

    - 위의 예에서, 항상 LockA를 먼저 얻고, 그 다음에 LockB를 얻는 순서로 수정하면, 데드락은 발생하지 않는다.

     

    Door2를 이렇게 바꾸면 된다.

    #include "pch.h"
    #include "Door2.h"
    #include "Door1.h"
    
    void Door2::GameStart()
    {
    	//get Lock A
    	Door1::Instance()->getLockA();
    
    	//get Lock B
    	lock_guard<mutex> guard(_LockB);
    }

     

     

    2. 싸이클 추적

    - 교착상태를 탐지하는 방법이다.

    - 교착상태가 발생하기 위한 조건은 스레드들이 원하는 lock이 싸이클을 형성하는 경우이다.

    thread1->thread2는 스레드1이 스레드2가 가진 lock을 필요로 하는 상황이다.

    이렇게, 스레드가 원하는 lock을 그래프로 그려보았을 때 싸이클이 형성된다면,

    교착상태가 발생한다.

     

    만약 사이클의 고리를 끊어주면

    thread5는 작업을 끝내고 thread4에게 lock을 줄 수 있고

    thread4는 작업을 끝내고 thread3에게 lock을 줄 수 있다.

    and so on.

     

    여튼... 그래서

    스레드를 사용할 때 그래프의 형태로 관리하며 교착상태가 발생하는 것을 탐지하고, 빠르게 수정할 수 있다.

     

    3. 신경쓰지 않는다.

    웃기지만 이건 정말 교착상태 해결책중 하나로 나온다.

     

    발생할 가능성이 적은 상황에서 그 적은 가능성때문에 많은 비용을 들여서 교착상태를 해결하기 보다는,

    그냥 무시하고 쓰다가 발생하면 빠르게 복구하겠다는 것이다.

     

    이렇게 찝찝하게 이번 포스팅은 끝난다.

     

    문제 제기만 하고, 명확한 해결책은 없는채로...

    728x90

    'C++ > C++ 멀티스레드' 카테고리의 다른 글

    C++ SpinLock구현  (0) 2022.10.09
    멀티스레드 Lock구현  (0) 2022.10.09
    C++ 멀티스레드, lock(mutex)  (1) 2022.10.06
    C++ 멀티스레드, Atomic  (0) 2022.10.05
    C++ 스레드 생성  (0) 2022.10.05

    댓글

Designed by Tistory.