콘텐츠로 건너뛰기
Home » 스레드의 시작 시점을 동기화하기

스레드의 시작 시점을 동기화하기

동시성 프로그래밍에서 동기화는 주로 한정된 자원을 두고 여러 스레드가 경쟁하지 않도록 락이나 세마포어를 사용해서 특정한 자원을 액세스하는 시점에서는 여러 스레드가 순차적으로 실행하도록 하는 것에 초점을 맞추고 있다. 하지만 이 외에도 각각의 스레드가 각자가 담당한 작업을 처리하기 위해 준비를 마치고, 다른 스레드의 준비를 기다렸다가 동시에 시작하도록 하는 기법도 필요하다. 이렇게 여러 스레드를 특정한 지점에서 기다리게 한 후 한 번에 깨워서 동시에 시작하게 하는 용도로 사용되는 동기화 프리미티브로는 이벤트와 배리어가 있다.

이런 기법이 가장 흔히 사용되는 경우로는 소켓 서버와 클라이언트를 하나의 스크립트에서 구현해서 스레드로 돌게 할 때이다. 서버의 소켓이 준비되기 전에 클라이언트들이 서버에 connect 될 수 없기 때문이다.

이벤트는 가장 단순한 동기화 프리미티브 중 하나로, 동시에 시작해야 하는 여러 스레드들이 “출발선”에서 이벤트 객체의 .wait() 메소드를 호출하고 대기상태에 들어가도록 한다. 그리고 어느 한 스레드에서 해당 이벤트 객체의 .set() 을 호출하면 해당 이벤트를 대기 중인 모든 스레드에서 wait() 메소드가 리턴되면서 각 스레드가 동시에 시작될 수 있다.

배리어도 비슷하게 여러 스레드를 기다리게하다 한 번에 깨우는 장치인데, 마치 정원이 다 차면 바로 출발하는 버스처럼 작동한다. 즉 이벤트를 기다리는 스레드들은 누군가가 깨워줘야 하는 것에 비해, 배리어 정해진 개수만큼의 스레드가 대기하게 되면 자동으로 해제되면서 동시에 깨어나게 된다.

이벤트 사용하기

이벤트를 사용하는 방법은 간단하다. 이벤트 객체는 별도의 인자 없이 생성한다. 이벤트를 대기할 스레드는 이벤트 객체의 .wait()를 호출해주면 되며, 외부 혹은 내부에서 누군가 .set() 을 호출하여 이벤트를 기다리는 스레드들을 한꺼번에 깨워줄 수 있다.

from threading import Thread, Event

def task(data, ev: Event=None):
  if ev:
    ev.wait()
  process_data(data)

ev = Event()
ts = [Thread(target=task, args=(x, ev)) for x in data]
for t in ts:
  t.start()
ev.set()

어떤 작업 데이터를 여러 스레드가 나누어 처리할 때, 먼저 생성된 스레드가 먼저 작업을 시작하는 것이 당연해지는데, 이 처럼 이벤트를 사용하면 스레드의 시작을 원하는 시점으로 동기화할 수 있다. 참고로 이벤트를 대기하는 Event.wait()에는 타임아웃을 따로 설정하지 않는다.

배리어 사용하기

배리어를 사용하는 방법은 간단하다. 다음과 같이 정원수(parties)를 지정하여 배리어를 생성한다. 그런 다음 각각의 스레드에서 배리어의 .wait() 메소드를 호출하면 된다. 간단히 서버와 클라이언트가 동시에 시작될 수 있게 하려면 다음과 같은 식으로 구성하면 된다. 서버의 준비 완료 시점이나 클라이언트 스레드의 생성 시점의 순서에 상관없이 배리어를 통해서 서버와 클라이언트가 동시에 작동하게 된다.

b = Barrier(2, timeout=5)

def server():
  start_server()
  b.wait()
  while True:
    connection = accept_connection()
    process_server_connection(connection)

def client():
  b.wait()
  while True:
    connection = make_connection()
    process_client_connection(connection)

배리어의 action 속성

배리어를 생성할 때에는 parties 외에 action=, timeout= 인자를 추가로 줄 수 있다. timeout 값은 배리어에서 대기하는 스레드의 최대 대기 시간에 대한 기본값이다. action은 함수로, 배리어가 릴리즈될 때 대기중인 스레드 중 하나에서 실행된다. (소스를 봐서는 마지막에 wait()하는 스레드인듯.)

컨디션 락이 개념상 락과 이벤트를 합친것이라고 하는데, 실제로 파이썬 표준 라이브러리에서의 배리어, 이벤트 구현은 내부적으로 컨디션 락을 사용해서 만들어지고 있다.