PEP 319 – Python 동기화/비동기화 블록
- Author:
- Michel Pelletier <michel at users.sourceforge.net>
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 24-Feb-2003
- Python-Version:
- 2.4
- Post-History:
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 Python에 ‘synchronize’와 ‘asynchronize’라는 두 개의 새로운 키워드를 추가할 것을 제안합니다.
결정
이 PEP는 PEP 343을 채택하면서 거부되었습니다.
- ‘synchronize’ 키워드
- Python에서 코드 동기화 개념은 너무 저수준입니다. 코드를 동기화하려면 프로그래머는 다음 의사 코드 패턴의 세부 사항을 알고 있어야 합니다.:
initialize_lock() ... acquire_lock() try: change_shared_data() finally: release_lock()
이 동기화 블록 패턴이 유일한 패턴은 아니지만(아래에서 더 다룹니다) 매우 흔하게 쓰입니다. 이 PEP는 위 코드를 다음과 같은 동등한 코드로 대체할 것을 제안합니다.:
synchronize: change_shared_data()
이 방식의 장점은 문법이 더 단순하고 사용자 실수의 여지가 더 적다는 것입니다. 현재 사용자는 ‘try/finally’ 블록 안에서 스레드 잠금을 획득하고 해제하는 코드를 작성해야 합니다. 이 코드에 오류가 있으면 해결하기 어렵기로 악명 높은 동시 스레드 잠금 문제가 생길 수 있습니다.
- ‘asynchronize’ 키워드
- ‘synchronize’ 코드 블록을 실행하는 도중에 프로그래머는 블로킹 입출력 루틴이나, 실행 시간을 가늠할 수 없고 동기화가 필요 없는 다른 작업을 실행하기 위해 잠시 비동기 실행으로 “되돌아가고” 싶을 수 있습니다. 이런 코드는 보통 다음 패턴을 따릅니다.:
initialize_lock() ... acquire_lock() try: change_shared_data() release_lock() # become async do_blocking_io() acquire_lock() # sync again change_shared_data2() finally: release_lock()
코드의 비동기 부분이 눈으로 보기에 잘 드러나지 않으므로 주석으로 표시해 둡니다. 제안된 ‘asynchronize’ 키워드를 사용하면 이 코드는 훨씬 깔끔해지고, 이해하기 쉬워지며, 오류가 생길 가능성도 줄어듭니다.:
synchronize: change_shared_data() asynchronize: do_blocking_io() change_shared_data2()
동기화되지 않은 블록 안에서 ‘asynchronize’ 키워드를 만나면 오류를 일으키거나 경고를 낼 수 있습니다(어차피 모든 코드 블록은 암묵적으로 비동기이기 때문입니다). 위 예제는 다음 코드와 같지 않다는 점에 유의해야 합니다.:
synchronize: change_shared_data() do_blocking_io() synchronize: change_shared_data2()
두 동기화 코드 블록이 루프의 같은 반복 안에서 실행될 수 있기 때문입니다. 다음을 살펴봅시다.:
while in_main_loop(): synchronize: change_shared_data() asynchronize: do_blocking_io() change_shared_data2()
여러 스레드가 이 코드를 반복 실행할 수 있습니다. ‘asynchronize’ 키워드가 없으면 블로킹 IO가 진행되는 동안 한 스레드가 루프 안에 머무르면서 동시에 잠금을 해제할 수 없습니다. 블로킹 IO를 수행하기 위해 메인 루프 안에서 잠금을 해제하는 이 패턴은 CPython 인터프리터 내부에서도 광범위하게 사용됩니다.
동기화 대상
제안된 대로라면 ‘synchronize’와 ‘asynchronize’ 키워드는 코드 블록을 동기화합니다. 하지만 프로그래머는 스레드가 동기화할 대상 객체를 지정하고 싶을 수 있습니다. 어떤 객체든 동기화 대상이 될 수 있습니다.
양방향 큐 객체를 생각해 봅시다. ‘get’ 메서드에서 두 큐를 각각 따로 동기화하기 위해 같은 ‘synchronize’ 코드 블록이 서로 다른 두 객체를 사용합니다.:
class TwoWayQueue:
def __init__(self):
self.front = []
self.rear = []
def putFront(self, item):
self.put(item, self.front)
def getFront(self):
item = self.get(self.front)
return item
def putRear(self, item):
self.put(item, self.rear)
def getRear(self):
item = self.get(self.rear)
return item
def put(self, item, queue):
synchronize queue:
queue.append(item)
def get(self, queue):
synchronize queue:
item = queue[0]
del queue[0]
return item
다음은 ‘synchronize’ 키워드가 없는 현재 Python에서 이와 동등한 코드입니다:
import thread
class LockableQueue:
def __init__(self):
self.queue = []
self.lock = thread.allocate_lock()
class TwoWayQueue:
def __init__(self):
self.front = LockableQueue()
self.rear = LockableQueue()
def putFront(self, item):
self.put(item, self.front)
def getFront(self):
item = self.get(self.front)
return item
def putRear(self, item):
self.put(item, self.rear)
def getRear(self):
item = self.get(self.rear)
return item
def put(self, item, queue):
queue.lock.acquire()
try:
queue.append(item)
finally:
queue.lock.release()
def get(self, queue):
queue.lock.acquire()
try:
item = queue[0]
del queue[0]
return item
finally:
queue.lock.release()
첫 번째 예제에서는 ‘synchronize’ 키워드가 잠금과 큐를 내부적으로 투명하게 연결해 주는 반면, 마지막 예제에서는 이 연결을 위해 별도의 클래스를 정의해야 했습니다.
동기화하는 그 밖의 패턴
‘synchronize’와 ‘asynchronize’ 키워드가 acquire나 release 같은 잠금 메서드의 사용을 완전히 대체할 수 없는 상황이 몇 가지 있습니다. 예를 들어 프로그래머가 acquire에 인자를 제공하려는 경우나, 아래에 나온 것처럼 한 코드 블록에서 잠금을 획득하고 다른 코드 블록에서 해제하는 경우가 있습니다.
다음은 ‘synchronize’와 ‘asynchronize’ 키워드를 모두 사용하도록 수정한 Zope의 클래스로, 서로 다른 코드 블록에서 획득되고 해제되어 ‘synchronize’를 사용하지 않는 명시적 잠금 풀도 사용합니다.:
import thread
from ZServerPublisher import ZServerPublisher
class ZRendevous:
def __init__(self, n=1):
pool=[]
self._lists=pool, [], []
synchronize:
while n > 0:
l=thread.allocate_lock()
l.acquire()
pool.append(l)
thread.start_new_thread(ZServerPublisher,
(self.accept,))
n=n-1
def accept(self):
synchronize:
pool, requests, ready = self._lists
while not requests:
l=pool[-1]
del pool[-1]
ready.append(l)
asynchronize:
l.acquire()
pool.append(l)
r=requests[0]
del requests[0]
return r
def handle(self, name, request, response):
synchronize:
pool, requests, ready = self._lists
requests.append((name, request, response))
if ready:
l=ready[-1]
del ready[-1]
l.release()
다음은 ‘Zope/ZServer/PubCore/ZRendevous.py’ 모듈에 있는 원래 클래스입니다. ‘_a’와 ‘_r’이라는 축약 이름의 “편의성”이 코드를 알아보기 어렵게 만듭니다.:
import thread
from ZServerPublisher import ZServerPublisher
class ZRendevous:
def __init__(self, n=1):
sync=thread.allocate_lock()
self._a=sync.acquire
self._r=sync.release
pool=[]
self._lists=pool, [], []
self._a()
try:
while n > 0:
l=thread.allocate_lock()
l.acquire()
pool.append(l)
thread.start_new_thread(ZServerPublisher,
(self.accept,))
n=n-1
finally: self._r()
def accept(self):
self._a()
try:
pool, requests, ready = self._lists
while not requests:
l=pool[-1]
del pool[-1]
ready.append(l)
self._r()
l.acquire()
self._a()
pool.append(l)
r=requests[0]
del requests[0]
return r
finally: self._r()
def handle(self, name, request, response):
self._a()
try:
pool, requests, ready = self._lists
requests.append((name, request, response))
if ready:
l=ready[-1]
del ready[-1]
l.release()
finally: self._r()
특히 accept 메서드의 asynchronize 부분은 그다지 명확하지 않습니다. 초보 프로그래머에게 ‘synchronize’와 ‘asynchronize’는 서로 다른 try/finally 블록에서 서로 다른 잠금에 대해 여러 acquire와 release 메서드를 다룰 때 마주치는 많은 문제를 없애 줍니다.
형식 문법
Python 문법은 Python 언어 레퍼런스 [1]에 기술된 수정된 BNF 문법 표기법으로 정의됩니다. 이 절에서는 이 문법을 사용하여 제안된 동기화 구문을 설명합니다.:
synchronize_stmt: 'synchronize' [test] ':' suite
asynchronize_stmt: 'asynchronize' [test] ':' suite
compound_stmt: ... | synchronized_stmt | asynchronize_stmt
(’…’는 생략된 다른 복합문을 나타냅니다.)
제안하는 구현
이 PEP의 작성자는 아직 구현을 검토하지 않았습니다. 해결해야 할 구현 문제가 몇 가지 있습니다. 주요 구현 문제는 동기화된 블록 동안 정확히 무엇이 잠기고 풀리는가 하는 것입니다.
한정되지 않은 동기화 블록(대상 인자 없이 ‘synchronize’ 키워드를 사용하는 경우) 동안에는 잠금을 생성하여 동기화된 코드 블록 객체와 연결할 수 있습니다. 블록을 실행하려는 모든 스레드는 먼저 코드 블록 잠금을 획득해야 합니다.
‘synchronize’ 블록 안에서 ‘asynchronize’ 키워드를 만나면, 내부 블록이 실행되기 전에 코드 블록 잠금이 풀리고 내부 블록이 종료되면 다시 잠깁니다.
동기화 블록의 대상이 지정되면 그 객체는 잠금과 연결됩니다. 이를 깔끔하게 구현하는 방법이 아마도 이 제안의 가장 큰 위험 요소일 것입니다. Java 가상 머신은 일반적으로 숨겨진 특수 잠금 객체를 대상 객체와 연관시키고, 이를 사용하여 대상을 둘러싼 블록만을 동기화합니다.
하위 호환성
하위 호환성은 새로운 from __future__ Python 문법(PEP 236)과 새로운 경고 프레임워크(PEP 230)를 통해 해결하며, 이를 통해 새 키워드 ‘synchronize’ 및 ‘asynchronize’와 충돌하는 이름을 단계적으로 제거하는 방향으로 Python 언어를 발전시킵니다. 지금 이 문법을 사용하려면 개발자는 다음 문장을 사용할 수 있습니다:
from __future__ import threadsync # or whatever
또한 키워드 ‘synchronize’나 ‘asynchronize’를 식별자로 사용하는 모든 코드에 대해 Python이 경고를 발생시킵니다. 적절한 기간이 지나면 이 문법은 표준이 되고, 위의 import 문은 아무 일도 하지 않게 되며, ‘synchronize’나 ‘asynchronize’라는 이름의 식별자는 예외를 발생시키게 됩니다.
PEP 310 신뢰할 수 있는 획득/해제 쌍
PEP 310은 ‘synchronize’와 같은 함수 역할을 할 수 있는 ‘with’ 키워드를 제안합니다(다만 ‘asynchronize’에 해당하는 기능은 없습니다). 다음 패턴은:
initialize_lock()
with the_lock:
change_shared_data()
제안된 다음 코드와 동등합니다.:
synchronize the_lock:
change_shared_data()
PEP 310은 기존 잠금에 대해 동기화해야 하는 반면, 이 PEP는 한정된 ‘synchronize’ 문 외에도 한정되지 않은 ‘synchronize’ 문이 전역적이고 내부적이며 투명한 잠금에 대해 동기화하도록 제안합니다. 또한 ‘with’ 문은 잠금 초기화가 필요한 반면, ‘synchronize’ 문은 잠금을 포함한 모든 대상 객체에 대해 동기화할 수 있습니다.
이런 식으로 제한되기는 하지만, ‘with’ 문은 더 추상적이며 동기화 외에도 더 많은 용도로 쓰입니다. 예를 들어, ‘with’ 키워드로 트랜잭션을 사용할 수 있습니다:
initialize_transaction()
with my_transaction:
do_in_transaction()
# when the block terminates, the transaction is committed.
‘synchronize’와 ‘asynchronize’ 키워드는 스레드 동기화를 제외하면 이 패턴은 물론 다른 어떤 일반적인 획득/해제 패턴에도 쓰일 수 없습니다.
Java의 방식
Java는 ‘synchronized’ 키워드를 정의하며(Java 키워드와 이 PEP의 ‘synchronize’는 문법적 시제가 다르다는 점에 유의하십시오), 이 키워드는 반드시 어떤 객체에 대해 한정되어야 합니다. 문법은 다음과 같습니다:
synchronized (Expression) Block
Expression은 ‘Block’이 동기화될 대상인 유효한 객체를 내놓아야 합니다(null은 오류를 발생시키며, 같은 이유로 ‘Expression’ 실행 중에 발생한 예외는 ‘synchronized’ 블록을 종료시킵니다).
Jython의 방식
Jython은 정적 메서드 ‘make_synchronized’를 가진 ‘synchronize’ 클래스를 사용합니다. 이 메서드는 호출 가능 객체 인자 하나를 받아, 그 인자를 감싸는 새로 생성된 동기화된 호출 가능 “래퍼”를 반환합니다.
Python에 제안하는 변경 사항 요약
언어에 새로운 ‘synchronize’ 및 ‘asynchronize’ 키워드를 추가합니다.
위험 요소
이 PEP는 Python 언어에 키워드 두 개를 추가할 것을 제안합니다. 이로 인해 기존 코드가 동작하지 않을 수 있습니다.
테스트할 구현이 없습니다.
이는 오늘날 Python 프로그래머가 직면한 가장 중요한 문제는 아닙니다(꽤 악명 높은 문제이기는 하지만).
이에 해당하는 Java 키워드는 과거분사형인 ‘synchronized’입니다. 이 PEP는 현재 시제인 ‘synchronize’가 Python의 정신에 더 부합한다고 보고 이를 제안합니다(Python은 Java보다 컴파일 시간과 실행 시간의 구분이 덜하기 때문입니다).
반대 의견
이 PEP는 python-dev에서 논의된 적이 없습니다.
참고 문헌
Copyright
This document has been placed in the public domain.