파이썬에는 스레드의 Lock
과 Event
를 섞은 듯한 기능을 하는 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()
receiver
는 notify
를 기다리는 스레드이며 sender
는 notifyAll
을 사용하여 모든 스레드에 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
을 사용하여 한 번에 하나의 스레드만 자원에 접근하도록 로직을 구현하여 자원을 관리하는 방법을 정리하였습니다. 하지만 파이썬에서는 자원의 접근을 관리하기 위해 Lock
뿐만 아니라 Mutex
, Semaphore
도 제공하고 있습니다.
Lock
은 스레드가 공통으로 사용하고 있는 자원에 접근할 때 무결성을 보장하기 위해 사용합니다. 해당 자원에 접근하기 위해 Lock
을 획득하고 해제하는 로직을 통해 한 스레드만 자원에 접근하도록 합니다. 단, Lock
은 하나의 프로세스에서 여러 개의 스레드를 생성하는데, 이 스레드들 사이에서만 사용될 수 있습니다.
Mutex
는 Lock
과 마찬가지로 한 번에 하나의 스레드만 자원에 접근하도록 하고, 기능과 자원에 대한 접근을 제한하는 방식이 유사합니다. 그런데 Mutex
는 프로세스 안에서만 유효한 것이 아니라 시스템 전반적으로 통용됩니다. 즉 Mutex
는 여러 프로세스 사이에서 사용되며, 다른 프로세스가 Mutex
를 소유하고 특정 영역을 점유하고 있다면 다른 프로세스는 모두 대기상태가 됩니다.
Semaphore
는 Mutex
를 확장한 것입니다. Mutex
와 Lock
은 한 번에 하나의 스레드만 점유할 수 있지만, Semaphore
는 정해진 개수만큼의 스레드가 점유할 수 있습니다. 예를 들어 Semaphore
의 최대 허용 스레드를 3개로 설정한다면, 시스템 전체를 통틀어 3개의 스레드가 이 자원을 이용할 수 있는 것입니다. 만약 최대 허용 개수를 1개로 설정하면 Mutex
와 같은 기능을 하게 됩니다.
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
을 상속받아 클래스로 구현하여 사용할 수도 있습니다.