본문 바로가기
개발/오늘의 개발일지

[Python] 하나의 변수에 여러 스레드가 접근하는 것을 막기 위한 Lock 이해하기

by 꾀돌이 개발자 2024. 11. 14.
반응형

 

여러 스레드가 하나의 변수에 접근하여 수정할 때, 발생할 수 있는 상황을 이해합니다.

 

 

 
 

 

 

목차

     

     

     

    개발 의도

    import threading
    
    # 공유 자원
    shared_variable = 0
    
    def increment():
        global shared_variable
        for _ in range(100000):
            shared_variable += 1
    
    # 여러 스레드 생성
    threads = []
    for _ in range(10):
        t = threading.Thread(target=increment)
        threads.append(t)
        t.start()
    
    # 모든 스레드가 종료될 때까지 기다림
    for t in threads:
        t.join()
    
    print("최종 값:", shared_variable)
    
    # 출력값: 906831(실행할 때마다 다름)

    - 위와 같이 10개의 스레드가 increment 함수를 실행하며

          shared_variable을 각각 100,000번 증가시키는 코드를 작성합니다.

     

    - 따라서 shared_variable의 최종 출력값은 10 * 100,000 = 1,000,000이 되어야 합니다.

     

    문제 상황

    - 코드를 실행했을 때, 1,000,000보다 작은 값이 출력됩니다. (가끔 1,000,000이 출력되기도 함)

     

    - 또한 코드를 실행할 때마다 다른 값이 출력됩니다.

     

    문제 원인 파악

    - 여러 스레드가 동시에 shared_variable += 1 연산을 수행하게 되어,

          결과 값이 의도한 값과 다르게 나올 수 있습니다.

     

    - 위 코드에서 shared_variable += 1 부분은 사실 여러 단계로 이루어져 있습니다.

          1. shared_variable의 현재 "값"을 읽음

          2. "값"을 1 증가시킴

          3. 증가된 "값"을 shared_variable에 다시 저장

     

    - 이 과정에서 여러 스레드가 동시에 shared_variable의 값을 읽으면,

           증가한 값이 덮어써지는 상황이 발생할 수 있습니다.

           결과적으로 최종 값이 기대한 값보다 작아지는 오류가 발생합니다.

           이러한 문제를 데이터 레이스(Data Race)라고 표현합니다.

     

    스레드 락(Lock)을 통한 문제 해결

    import threading
    
    # 공유 자원
    shared_variable = 0
    lock = threading.Lock()
    
    def increment():
        global shared_variable
        for _ in range(100000):
            with lock:  # Lock을 사용해 동기화
                shared_variable += 1
    
    # 여러 스레드 생성
    threads = []
    for _ in range(10):
        t = threading.Thread(target=increment)
        threads.append(t)
        t.start()
    
    # 모든 스레드가 종료될 때까지 기다림
    for t in threads:
        t.join()
    
    print("최종 값:", shared_variable)

    - threading 모듈에서 제공하는 Lock 객체를 사용하여

          여러 스레드가 같은 변수에 동시에 접근하지 않도록 막을 수 있습니다.

     

    - lock = threading.Lock() 코드를 통해 락 객체를 생성합니다.

          이 객체는 한 번에 하나의 스레드만 shared_variable에 접근할 수 있도록 제어하는 역할을 합니다.

     

    - with lock 코드를 사용해 임계 구역을 정의합니다.

           임계 구역(critical section)은 여러 스레드가 동시에 접근하면 안 되는 코드 영역을 의미합니다.

           with 블록 안에서 락이 획득되고, 블록을 벗어나면 락이 해제됩니다.

     

    스레드 락(Lock)을 사용한 이유

    - 여러 스레드가 동시에 shared_variable을 수정할 경우,

          데이터 레이스(Data Race)가 발생할 수 있습니다.

          예를 들어, 두 스레드가 동시에 shared_variable += 1을 수행하면

                 연산 결과가 의도한 값과 다를 수 있습니다.

     

    - lock을 사용하면 한 번에 하나의 스레드만 shared_variable을 수정할 수 있도록 보장됩니다.

           lock이 획득되었을 때 다른 스레드는 락이 해제될 때까지 대기합니다.

     

    with lock 구문

    lock.acquire()
    try:
        # 임계 구역
        shared_variable += 1
    finally:
        lock.release()

    - 위 코드는 with lock 구문과 동일한 역할을 하지만,

          with를 사용하는 방식이 더 직관적이고 안전합니다.

     

    - with lock 구문은 lock.acquire()와 lock.release()를 간단하게 사용할 수 있게 해줍니다.

     

    아래 코드로 디버깅 완료

    import threading
    
    # 공유 자원
    shared_variable = 0
    lock = threading.Lock()
    
    def increment():
        global shared_variable
        for _ in range(100000):
            with lock:  # Lock을 사용해 동기화
                shared_variable += 1
    
    # 여러 스레드 생성
    threads = []
    for _ in range(10):
        t = threading.Thread(target=increment)
        threads.append(t)
        t.start()
    
    # 모든 스레드가 종료될 때까지 기다림
    for t in threads:
        t.join()
    
    print("최종 값:", shared_variable)

    - 이 방법을 통해 데이터 레이스를 방지하고 원하는 thread 연산을 수행할 수 있습니다.

     

    - 단,  스레드들이 락이 해제될 때까지 대기하는 시간으로 인해

          lock을 사용했을 때, 연산 속도가 조금 느려집니다.

    반응형