Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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’ 키워드를 만나면 오류를 발생시키거나 경고를 표시할 수 있습니다(모든 코드 블록은 어차피 암시적으로 비동기이기 때문입니다). 위 예제가 not과 동일하지 않다는 점에 유의하는 것이 중요합니다.:

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’ 키워드가 없으면 블로킹 I/O가 진행되는 동안 한 스레드가 루프에 머무르면서 동시에 잠금을 해제할 수 없습니다. 주 루프 내부에서 잠금을 해제하여 블로킹 I/O를 수행하는 이 패턴은 CPython 인터프리터 자체에서도 광범위하게 사용됩니다.

동기화 대상

제안된 방식에서는 ‘synchronize’와 ‘asynchronize’ 키워드가 코드 블록을 동기화합니다. 그러나 프로그래머는 스레드가 동기화할 대상 객체를 지정하고 싶을 수 있습니다. 어떤 객체든 동기화 대상이 될 수 있습니다.

양방향 큐 객체를 생각해 보십시오. 동일한 ‘synchronize’ 코드 블록이 두 개의 서로 다른 객체를 사용하여 ‘get’ 메서드에서 두 큐를 각각 동기화합니다.:

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’ 키워드가 acquirerelease 같은 락 메서드의 사용을 완전히 대체할 수 없는 몇 가지 상황이 있습니다. 프로그래머가 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 블록에서 서로 다른 락에 대해 여러 개의 acquirerelease 메서드를 다루면서 겪게 되는 많은 문제를 없애줍니다.

형식 구문

파이썬 구문은 Python Language Reference [1]에 설명된 수정된 BNF 문법 표기법으로 정의됩니다. 이 절은 이 문법을 사용하여 제안된 동기화 구문을 설명합니다:

synchronize_stmt: 'synchronize' [test] ':' suite
asynchronize_stmt: 'asynchronize' [test] ':' suite
compound_stmt: ... | synchronized_stmt | asynchronize_stmt

(’…’는 생략된 다른 복합문을 나타냅니다).

제안된 구현

이 PEP의 저자는 아직 구현을 탐구하지 않았습니다. 해결해야 할 몇 가지 구현 문제가 있습니다. 주된 구현 문제는 동기화된 블록 동안 정확히 무엇이 잠기고 풀리는가입니다.

한정되지 않은 동기화된 블록(대상 인자 없이 ‘synchronize’ 키워드를 사용하는 것) 동안에는 락이 생성되어 동기화된 코드 블록 객체와 연관될 수 있습니다. 그 블록을 실행하려는 모든 스레드는 먼저 코드 블록 락을 획득해야 합니다.

‘synchronize’ 블록 안에서 ‘asynchronize’ 키워드를 만나면, 내부 블록이 실행되기 전에 코드 블록 락이 풀리고 내부 블록이 종료될 때 다시 잠깁니다.

동기화된 블록의 대상이 지정되면 그 객체는 락과 연관됩니다. 이것을 깔끔하게 구현하는 방법이 아마 이 제안의 가장 큰 위험 요소일 것입니다. 자바 가상 머신은 일반적으로 특수한 숨겨진 락 객체를 대상 객체와 연관시키고, 이를 사용하여 대상을 둘러싼 블록만을 동기화합니다.

하위 호환성

하위 호환성은 새로운 from __future__ 파이썬 구문(PEP 236)과 새로운 경고 프레임워크(PEP 230)로 해결되며, 이를 통해 새로운 키워드 ‘synchronize’와 ‘asynchronize’를 사용하는 충돌하는 이름을 단계적으로 폐지하는 방향으로 파이썬 언어를 발전시킵니다. 지금 이 구문을 사용하려면, 개발자는 다음 문을 사용할 수 있습니다:

from __future__ import threadsync  # or whatever

또한, ‘synchronize’나 ‘asynchronize’ 키워드를 식별자로 사용하는 모든 코드는 파이썬으로부터 경고를 받게 됩니다. 적절한 기간이 지나면 이 구문은 표준이 되며, 위의 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은 유효한 객체를 산출해야 하며(null은 오류를 일으키고 ‘Expression’ 중의 예외도 같은 이유로 ‘synchronized’ 블록을 종료시킵니다), 그 객체에 대해 ‘Block’이 동기화됩니다.

Jython은 어떻게 하는가

Jython은 ‘synchronize’ 클래스와 정적 메서드 ‘make_synchronized’를 사용하는데, 이 메서드는 하나의 호출 가능 객체 인자를 받아, 인자를 감싸는 새로 생성된 동기화된 호출 가능 객체 “래퍼”를 반환합니다.

Python에 제안된 변경 사항 요약

언어에 새로운 ‘synchronize’와 ‘asynchronize’ 키워드를 추가.

위험 요소

이 PEP는 Python 언어에 두 개의 키워드를 추가할 것을 제안합니다. 이는 코드를 깨뜨릴 수 있습니다.

테스트할 구현이 없습니다.

이는 오늘날 파이썬 프로그래머들이 직면한 가장 중요한 문제는 아닙니다(다만 상당히 악명 높은 문제이기는 합니다).

이에 대응하는 자바 키워드는 과거분사형인 ‘synchronized’입니다. 이 PEP는 현재형인 ‘synchronize’를 제안하는데, 이는 파이썬(컴파일 타임과 런타임 사이의 구분이 자바보다 적음)의 정신에 더 부합하기 때문입니다.

반대 의견

이 PEP는 python-dev에서 논의된 적이 없습니다.

참고 자료