[CS/Python]Thread(3)-Condition, Lock, Mutex, Semaphore, Local Data

Jay·2023년 1월 26일
0
post-thumbnail

Thread Condition

파이썬에는 스레드의 LockEvent를 섞은 듯한 기능을 하는 Condition이라는 기능이 제공됩니다. Condition을 사용하면 모든 스레드가 lock을 잡은 것처럼 동작을 멈추게 됩니다. 그리고 notify를 받게되면 해당 Condition의 스레드는 다시 동작을 수행하게 됩니다. Lock의 경우에는 다른 스레드에서 Lock을 해제해야 다른 스레드에서 Lock을 획득하여 로직을 수행하는 반면, Condition에서는 이를 notify로 대체하게 됩니다.

import time
import logging
import threading

logging.basicConfig(level=logging.DEBUG, format="(%(threadName)s) %(message)s")

def receiver(condition):
    logging.debug("Start receiver")

    with condition:
        logging.debug("waiting...")
        condition.wait()				# condition이 notify/timeout 될때까지 대기
        time.sleep(0.5)
        logging.debug("end")

def sender(condition):
    logging.debug("Start sender")

    with condition:
        logging.debug("send notify")
        condition.notifyAll()			# 모든 스레드를 notify
        logging.debug("end")

def main():
    condition = threading.Condition()

    for i in range(5):
        t = threading.Thread(target=receiver, name="Receiver %i"%i, args=(condition,))
        t.start()

    send = threading.Thread(target=sender, name="Sender", args=(condition,))

    time.sleep(1)

    with condition:
        condition.notify(1)				# 인자로 입력받은 개수만큼의 스레드를 notify

    time.sleep(1)
    send.start()

if __name__ == "__main__":
    main()

receivernotify 를 기다리는 스레드이며 sendernotifyAll을 사용하여 모든 스레드에 notify하는 스레드입니다.

(Receiver 0) Start receiver
(Receiver 1) Start receiver
(Receiver 2) Start receiver
(Receiver 3) Start receiver
(Receiver 4) Start receiver
(Receiver 0) waiting...
(Receiver 1) waiting...
(Receiver 2) waiting...
(Receiver 3) waiting...
(Receiver 4) waiting...
(Receiver 0) end				# notify(1)로 인한 recevier 종료
(Sender) Start sender
(Sender) send notify			# noftifyAll() 호출 -> 모든 컨디션에 notify
(Sender) end
(Receiver 4) end
(Receiver 2) end
(Receiver 3) end
(Receiver 1) end

Lock, Mutex, Semaphore

지난 포스트에서 Lock을 사용하여 한 번에 하나의 스레드만 자원에 접근하도록 로직을 구현하여 자원을 관리하는 방법을 정리하였습니다. 하지만 파이썬에서는 자원의 접근을 관리하기 위해 Lock 뿐만 아니라 Mutex, Semaphore도 제공하고 있습니다.

Lock

Lock은 스레드가 공통으로 사용하고 있는 자원에 접근할 때 무결성을 보장하기 위해 사용합니다. 해당 자원에 접근하기 위해 Lock을 획득하고 해제하는 로직을 통해 한 스레드만 자원에 접근하도록 합니다. 단, Lock은 하나의 프로세스에서 여러 개의 스레드를 생성하는데, 이 스레드들 사이에서만 사용될 수 있습니다.

Mutex

MutexLock과 마찬가지로 한 번에 하나의 스레드만 자원에 접근하도록 하고, 기능과 자원에 대한 접근을 제한하는 방식이 유사합니다. 그런데 Mutex는 프로세스 안에서만 유효한 것이 아니라 시스템 전반적으로 통용됩니다.Mutex는 여러 프로세스 사이에서 사용되며, 다른 프로세스가 Mutex를 소유하고 특정 영역을 점유하고 있다면 다른 프로세스는 모두 대기상태가 됩니다.

Semaphore

SemaphoreMutex를 확장한 것입니다. MutexLock은 한 번에 하나의 스레드만 점유할 수 있지만, Semaphore는 정해진 개수만큼의 스레드가 점유할 수 있습니다. 예를 들어 Semaphore의 최대 허용 스레드를 3개로 설정한다면, 시스템 전체를 통틀어 3개의 스레드가 이 자원을 이용할 수 있는 것입니다. 만약 최대 허용 개수를 1개로 설정하면 Mutex와 같은 기능을 하게 됩니다.


Thread Local Data

Lock, Mutex 그리고 Semaphore로 자원을 공유하는 방법을 정리하였습니다. 하지만 반대로 자원을 공유하지 않고 각 스레드에서 별도로 사용할 자원들이 필요한 경우가 있습니다. 이처럼 각 스레드 별로 한정되는 데이터를 위해 파이썬에서는 Thread Local Data를 제공하고 있습니다.

import logging
import threading

logging.basicConfig(level=logging.DEBUG, format="(%(threadName)s) %(message)s")

def print_local_data(local_data):
    try:
        data = local_data.index
    except:
        logging.debug("Value not set yet.")
    else:
        logging.debug("value : %s" % data)

def set_local_data(local_data, index):
    print_local_data(local_data)
    local_data.index = index
    print_local_data(local_data)

def main():
    local_data = threading.local()			# local 클래스로 local_data 변수 선언
    print_local_data(local_data)
    local_data.index = 0
    print_local_data(local_data)

    for i in range(5):
        t = threading.Thread(target=set_local_data, name=("thread-%s" % i), args=(local_data, i+1))
        t.start()

if __name__ == "__main__":
    main()

threading모듈에서 제공하는 local클래스를 이용해 각 스레드에서 사용할 데이터를 담을 변수인 local_data를 선언하였습니다. 그리고 5개의 스레드에 인자로 전달하여 각 스레드에서 local_data의 값을 출력하고 종료하도록 구현하였습니다.

(MainThread) Value not set yet.
(MainThread) value : 0				# 먼저 실행된 main 스레드에서 0 할당
(thread-0) Value not set yet.		# 하지만 이후 실행된 스레드에서는 이를 공유하지 않음
(thread-1) Value not set yet.
(thread-2) Value not set yet.
(thread-3) Value not set yet.
(thread-4) Value not set yet.
(thread-0) value : 1				# 각 스레드에서 할당된 값을 출력
(thread-1) value : 2
(thread-2) value : 3
(thread-3) value : 4
(thread-4) value : 5

만약 local_data의 값이 할당되었으면 해당 값을 출력하고, 할당되지 않았다면 Value not set yet. 메세지를 출력합니다. 출력 결과를 보게되면 처음 실행된 메인 스레드에서 local_data가 할당되지 않아 0을 할당하고 출력하였습니다. 만약 local_data가 여러 스레드와 공유된다면 그 다음에 실행되는 스레드에서는 할당된 값인 0이 먼저 나온 후, 새로 할당된 값이 나와야 합니다. 하지만 각 스레드에서는 local_data에 할당된 값이 없다는 메세지를 로깅한 다음, 각 스레드에서 할당한 값을 출력하였습니다.

이처럼 각 스레드가 local_data를 공유하지 않는 것을 확인하였습니다. 이처럼 스레드 별로 고유의 데이터를 사용하려면 threading 모듈의 local 클래스를 사용하여 구현하면 됩니다. 위의 예제에서는 변수처럼 사용하였지만, threading.local을 상속받아 클래스로 구현하여 사용할 수도 있습니다.





reference

https://docs.python.org/ko/3/library/threading.html

0개의 댓글