Covenant

파이썬 코드로 보는 Lock, DeadLock, Race Condition



이런 상황이 꼼짝달싹 못하는 상태 즉 데드락 상황입니다.


비유적으로 위 사진과 같은 상황이 컴퓨터 환경에서 일어납니다. 바로 교착생태(데드락, Deadlock)라고 불리는 상황입니다. 앞 혹은 뒤의 자동차가 비켜주어야 앞으로 나갈 수 있는데 다른 자동차 또한 앞 혹은 뒤 차가 비켜주어야합니다. 이렇게 둘 이상의 프로세스가 공유 자원을 획득하지 못해 더 이상 실행할 수 없고 무한정 기다려야하는 상황을 말합니다.


데드락 상황을 막기 위해서 세마포어(Semaphore)와 뮤택스(Mutex)를 사용합니다. 세마포어는 네덜란드 컴퓨터 과학자인 에츠허르 데이크스트라가 고안한 방법으로 멀티프로세스 환경에서 공유 자원 접근 제한하기 위한 방법으로 사용합니다. 뮤텍스는 공유된 자원의 데이터를 여러 스레드가 접근하는 것을 막는 것입니다. 스레드 동기화(Thread synchronization)를 통해서 안정적으로 동작하게 처리합니다.


  • 세마포어와 뮤텍스의 차이점
    • 교착상태 예방하기 위한 동기화 작업
    • 세마포어와 뮤텍스 개체는 모두 병렬 프로그래밍 환경에서 상호 배제를 위해 사용
    • 뮤텍스 개체는 단일 스레드가 리소스 또는 중요 섹션을 소비 허용
    • 세마포는 리소스에 대한 제한된 수의 동시 액세스를 허용


문제상황


공유 자원의 선점, 비선점에 대한 문제와 해결하는 방법을 살펴보겠습니다.



라이언, 무지, 어파치가 카카오뱅크에서 계좌 하나 만들고 각각 1000원씩 입금하려고 합니다. 라이언, 무지, 어파치가 동시에 입금한다고 할때 입금 코드를 통해서 어떤 문제가 발생할 수 있을지 살펴보겠습니다.



문제상황 코드

import logging
from concurrent.futures import ThreadPoolExecutor
import time

class KakaoBank:
    # 공유 변수(value)
    def __init__(self):
        self.money = 0

    def deposit_1000won(self, user_name):
        print("Thread {}: 입금 시작합니다.".format(user_name))

        # 공유변수(money)에 스래드가 동시에 읽고 쓸 수 있다.
        local_data = self.money
        local_data += 1000 
        time.sleep(0.1)
        self.money = local_data

        print("Thread {}: 입금 종료합니다.".format(user_name))


if __name__ == "__main__":
    bank = KakaoBank()

    print("카카오뱅크 계좌를 생성하였습니다. 현재 잔액: {}원".format(bank.money))

    with ThreadPoolExecutor(max_workers=2) as executor:
        for user_name in ['라이언', '무지', '어파치']:
            executor.submit(bank.deposit_1000won, user_name)

    print("카카오뱅크 계좌 현재 잔액: {}원".format(bank.money))
21:02:45: 카카오뱅크 계좌를 생성하였습니다. 현재 잔액: 0원
21:02:45: [Thread 라이언] 입금 시작합니다.
21:02:45: [Thread 무지] 입금 시작합니다.
21:02:45: [Thread 라이언] 입금 종료합니다.
21:02:45: [Thread 어파치] 입금 시작합니다.
21:02:45: [Thread 무지] 입금 종료합니다.
21:02:45: [Thread 어파치] 입금 종료합니다.
21:02:45: 카카오뱅크 계좌 현재 잔액: 2000원

라이언 무지 어파치가 1000원씩 입금하였기에 3000원이 나와야합니다. 그런데 실행결과 2000원 나옵니다. 실제 이렇게 됬다면 카카오뱅크는 신뢰를 잃고 망했을것입니다. 어떻게 된 상황일까요?

  • for user_name in ['라이언', '무지', '어파치']: 를 통해서 라이언, 무지, 어파치 순으로 deposit_1000won 메서드를 통해서 1000원을 계좌에 입금할 것입니다.
  • deposit_1000won 메서드는 멀티쓰레드로 동작합니다. 거의 동시에 라이언, 무지, 어파치가 deposit_1000won 메서드를 실행할 것입니다.
  • self.money의 값을 읽어와서 1000원을 더하고 self.money에 저장하기 전에 다른 누군가 self.money에 저장하였기에, 이런 상황이 발생하는 것입니다.


문제 상황을 그림으로 표현하면 위와 같습니다. 라이언이 업데이트한 공유변수의 값이 아닌 과거 공유변수 값을 가지고 입금을 하였기에 문제가 발생합니다.



Lock을 이용한 해결


Lock을 이용한 해결 코드

import logging
from concurrent.futures import ThreadPoolExecutor
import time
import threading

class KakaoBank:
    # 공유 변수(value)
    def __init__(self):
        self.money = 0
        self._lock = threading.Lock()

    # 변수 업데이트 함수
    # 스택 영역에 저장 - 자기 자신의 영역
    def deposit_1000won(self, user_name):
        print("Thread {}: 입금 시작합니다.".format(user_name))

        self._lock.acquire() # Lock 획득

        local_copy = self.money
        local_copy += 1000
        time.sleep(0.1)
        self.money = local_copy

        self._lock.release() # Lock 반환

        print("Thread {}: 입금 종료합니다.".format(user_name))


if __name__ == "__main__":
    bank = KakaoBank()

    print("카카오뱅크 계좌를 생성하였습니다. 현재 잔액: {}원".format(bank.money))

    with ThreadPoolExecutor(max_workers=2) as executor:
        for user_name in ['라이언', '무지', '어파치']:
            executor.submit(bank.deposit_1000won, user_name)

    print("카카오뱅크 계좌 현재 잔액: {}원".format(bank.money))
카카오뱅크 계좌를 생성하였습니다. 현재 잔액: 0원
Thread 라이언: 입금 시작합니다.
Thread 무지: 입금 시작합니다.
Thread 라이언: 입금 종료합니다.
Thread 어파치: 입금 시작합니다.
Thread 무지: 입금 종료합니다.
Thread 어파치: 입금 종료합니다.
카카오뱅크 계좌 현재 잔액: 3000원
  • 세 친구들이 각각 1000원씩 입급하였기에 최종적으로 3000원이 잘 나옵니다.
  • deposit_1000won에서 공유 자원인 self.money에 접근할 때 lock을 걸어서 다른 스래드가 접근하지 못하도록 막습니다.


다른 방법으로

def deposit_1000won(self, user_name):
    print("Thread {}: 입금 시작합니다.".format(user_name))

    with self._lock:
        local_copy = self.money
        local_copy += 1000
        time.sleep(0.1)
        self.money = local_copy

    print("Thread {}: 입금 종료합니다.".format(user_name))
  • 이전 코드는 acquire, release를 사용해서 lock을 걸어주었습니다.
  • 코드가 길어진다면 release를 했는지 어디서 release가 일어나는지 가독성에 문제가 생깁니다.
  • with self._lock:을 사용하면 들여쓰기 부분에 lock이 걸립니다.