=========================================================== 소프트웨어 트랜잭셔널 메모리(Software Transactional Memory) =========================================================== .. contents:: **경고:** 아래 내용은 다소 오래되었으며, ``pypy-stm``\ 변형은 더 이상 활발히 개발되고 있지 않습니다. 기술적 세부 사항과 이 접근 방식의 장단점에 대한 설명을 원하시면, Remi Meier의 2019년 `논문`__\ 과 그의 `박사 학위 논문`__\ 을 읽어보실 수 있습니다. .. __: https://dl.acm.org/doi/abs/10.1145/3359619.3359747 .. __: https://www.research-collection.ethz.ch/handle/20.500.11850/371019 이 페이지는 동일한 프로세스에서 여러 개의 독립적인 CPU 집약적 스레드를 병렬로 실행할 수 있는 PyPy의 특별한 개발 중 버전인 ``pypy-stm``\ 에 관한 것입니다. 이는 Python 세계에서 "전역 인터프리터 락(GIL)" 문제로 알려진 것에 대한 해결책입니다 --- 즉 GIL 없는 Python 구현입니다. "STM"은 내부적으로 사용되는 기법인 소프트웨어 `Transactional Memory`_\ 의 약자입니다. 이 페이지는 사용자의 관점에서 ``pypy-stm``\ 을 설명하고, 진행 중인 작업을 설명하며, 마지막으로 더 자세한 구현 세부사항에 대한 참고 자료를 제공합니다. 이 작업은 Remi Meier와 Armin Rigo가 수행했습니다. 지금까지 이 작업에 크라우드 펀딩을 해주신 모든 후원자분들께 감사드립니다! `2nd call for donation`_\ 을 살펴봐 주십시오. .. _`Transactional Memory`: https://en.wikipedia.org/wiki/Transactional_memory .. _`2nd call for donation`: https://pypy.org/tmdonate2.html pypy-stm의 용도 =============== ``pypy-stm``\ 는 일반 PyPy 인터프리터의 변형입니다. (이 버전은 Python 2.7을 지원합니다. `Python 3, CPython, 그리고 기타`_\ 는 아래를 참고하십시오.) 아래에 나열된 caveats_\ 를 감안하면, 이론적으로는 두 경우 모두 JIT 버전을 비교했을 때 일반 PyPy보다 20%~50% 느린 정도여야 합니다(단, 아래 내용을 참고하십시오!). 이것은 소프트웨어 트랜잭셔널 메모리(Software Transactional Memory)의 약자인 STM이라고 불리며, 이는 내부적으로 사용되는 기법입니다(`구현 세부 사항 참조`_\ 참고). 그 결과로 나온 ``pypy-stm``\ 이 Python 코드의 여러 스레드를 병렬로 실행할 수 있다는 것이 이점입니다. 두 개 이상의 스레드를 병렬로 실행하는 프로그램은 이상적으로는 일반 PyPy보다(지금이든, 버그가 곧 수정된 후든) 더 빠르게 실행되어야 합니다. * ``pypy-stm``\ 은 GIL 기반 PyPy와 완전히 호환되므로, 대체품으로 그대로 사용할 수 있으며 멀티스레드 프로그램이 여러 코어에서 실행됩니다. * ``pypy-stm``\ 은 순수 Python 모듈 ``transaction``\ 에서 사용자에게 특별한 API를 제공합니다(강제하지는 않습니다). 이 모듈은 더 저수준의 모듈인 ``pypystm``\ 을 기반으로 하지만, non-STM PyPy나 CPython과의 호환성도 일부 제공합니다. * GIL이 제거되는 방식을 기반으로, `멀티스레드 프로그램 작성법: 10,000피트 상공에서 바라보기`_\ 와 `transaction.TransactionQueue`_\ 에 대해 이야기하겠습니다. ...그리고 pypy-stm이 무엇을 위한 것이 아닌지 -------------------------------------------- ``pypy-stm``\ 은 GIL 없는 Python을 제공합니다. 이는 GIL이 애초에 문제가 되는 상황에서 유용하다는 것을 의미합니다. (여기에는 프로그램을 여러 스레드로 실행하도록 쉽게 수정할 수 있는 경우도 포함됩니다. 흔히 우리는 바로 그 GIL 때문에 그렇게 하는 것을 고려하지 않습니다.) 그러나 GIL이 문제가 되지 않는 경우도 많습니다. 이런 경우에는 ``pypy-stm``\ 이 도움이 될 것이라고 기대하지 마십시오! 여기에는 멀티스레드를 사용하지만 실제로는 파이썬 코드를 실행하는 데 많은 시간을 쓰지 않는 모든 프로그램이 포함됩니다. 예를 들어, I/O가 발생하기를 기다리는 데 모든 시간을 쓰거나, 거대한 행렬에 대해 오래 걸리는 계산을 수행하고 있을 수 있습니다. 이는 CPU가 유휴 상태이거나 어차피 C/Fortran 라이브러리 안에 있는 경우이며, 두 경우 모두 인터프리터(CPython이든 일반 PyPy든)는 외부 호출을 수행하는 동안 GIL을 해제해야 합니다. 따라서 스레드들이 GIL을 두고 다투게 되지는 않습니다. 시작하기 ======== **pypy-stm은 현재 64비트 Linux가 필요합니다.** 개발은 `stmgc-c8`_\ 브랜치에서 진행됩니다. 그저 시험해 보는 데에만 관심이 있으시다면, 최근 빌드된 바이너리를 저희가 업로드할 때까지 계속 요청해 주십시오. 현재 버전은 네 개의 "세그먼트"를 지원하며, 이는 최대 네 개의 스레드를 병렬로 실행한다는 의미입니다. 소스로부터 빌드하려면, 먼저 커스텀 버전의 gcc(!)를 컴파일해야 합니다. 여기 안내를 참고하십시오: https://bitbucket.org/pypy/stmgc/src/default/gcc-seg-gs/ (이 패치들은 gcc에 통합되는 중이라는 점에 유의하십시오. 향후 버전의 gcc는 더 이상 패치가 필요하지 않을 가능성이 있습니다.) 그런 다음 PyPy의 `stmgc-c8`_ 브랜치를 받아서 다음을 실행합니다.:: cd pypy/goal ../../rpython/bin/rpython -Ojit --stm 마지막으로, 이 과정은 ``gcc-seg-gs``\ 를 호출하여 생성된 C 코드를 컴파일하려고 시도하며, 이는 위 안내에서 설치한 스크립트여야 합니다. .. _`stmgc-c8`: https://bitbucket.org/pypy/pypy/src/stmgc-c8/ .. _caveats: 현재 상태(stmgc-c7) ------------------- .. warning:: 이 페이지는 오래되었습니다. 나머지 내용은 STMGC-C7\ 에 관한 것이지만, 현재 개발 작업은 STMGC-C8\ 에서 진행되고 있습니다. * **NEW:** 이제 더 이상 충돌 없이 잘 작동하는 것 같습니다. 발견하신 크래시(또는 다른 버그)가 있으면 `report any crash`_\ 해 주십시오. * "richards"와 같은 예제에서는 오버헤드가 20%밖에 되지 않을 정도로 낮게 실행됩니다. 더 높은 오버헤드를 보이는 다른 예제들도 있습니다 --현재 "translate.py"의 경우 최대 2배에 달합니다-- 이에 대해서는 아직 원인을 파악하려고 노력하는 중입니다. 그 원인으로 의심되는 것 중 하나는 우리의 부분 GC 구현이며, 아래를 참고하십시오. * **NEW:** ``PYPYSTM`` 환경 변수와 ``pypy/stm/print_stm_log.py`` 스크립트를 사용하면 정확히 어떤 "충돌"이 발생했는지 알 수 있습니다. 이는 아래의 `transaction.TransactionQueue`_\ 절에 설명되어 있습니다. * **새로운 점:** (``stmdict``\ 와 같은) 트랜잭션 친화적인 특수 API로, 아래 `transaction.TransactionQueue`_\ 절에서 설명합니다. 기존 API는 다시 변경되었으며, 대부분 다른 모듈로 옮겨졌습니다. 이 점에 대해 죄송합니다. 나중에 나쁜 API에 발이 묶이는 것보다는 API를 일찍 변경하는 것이 더 나은 생각이라고 생각합니다... * 현재 RAM 1.5GB로 제한되어 있습니다(이는 `core.h`__\ 에 있는 매개변수일 뿐입니다 -- 이론적으로는요. 실제로는 이 값을 너무 늘리면 clang이 다시 크래시합니다). 메모리 오버플로는 제대로 처리되지 않으며, 세그폴트를 일으킵니다. * **NEW:** JIT의 준비(warm-up) 시간이 다시 개선되었지만, 여전히 상대적으로 깁니다. 머신 코드를 생성하기 위해, JIT은 "inevitable" 모드로 진입해야 합니다. 이는 프로그램이 여러 초 동안 실행되지 않으면 성능이 나쁘게 나타난다는 것을 의미하며, 여기서 *여러*\ 는 *많음*\ 을 의미할 수도 있습니다. 벤치마크를 시도할 때는 준비(warm-up) 상태, 즉 성능이 더 이상 향상되지 않는 상태에 도달했는지 반드시 확인하십시오. * 이 GC는 새로 만들어진 것으로, PyPy의 일반 GC에서 명백히 영감을 받았지만 아직 여러 최적화가 빠져 있습니다. 즉시 소멸하지 않는 작은 객체를 대량으로 할당하는 프로그램(흔히 있는 상황입니다)은 이러한 누락된 최적화로 인해 성능 저하를 겪습니다. (최신판 ``stmgc-c8``\ 은 이 부분에서 더 낫습니다.) * 약한 참조(weakref)는 현재로서는 다소 이상하게 동작하는 것처럼 보일 수 있는데, ``gc.collect()``\ 를 거쳐도 계속 살아있는 경우가 있거나, 심지어 죽었다가 잠시 되살아난 후 다시 죽는 경우도 있습니다. 비슷한 문제가 일부 외부 자원 접근에서도 가끔 다른 곳에서 나타날 수 있는데, (겉보기의) 직렬화된 순서가 실제 (멀티스레딩) 순서와 일치하지 않는 경우입니다. 이는 버그입니다(``stmgc-c8``\ 에서 일부는 이미 수정되었습니다). 또한 ``weakref.getweakrefcount()``\ 와 같은 디버깅 도우미가 잘못된 답을 줄 수도 있습니다. * STM 시스템은 매우 효율적인 읽기/쓰기 배리어(read/write barrier)를 기반으로 하며, 이는 대부분 완료되었습니다(JIT\ 가 생성한 머신 코드에서 배치는 조금 더 개선될 수 있습니다). * 프로세스를 포크(fork)하는 것은 전체 메모리를 수동으로 복사해야 하기 때문에 느립니다. 이에 대한 경고가 출력됩니다. * 매우 오래 실행되는 프로세스(수일 단위)는 결국 내부 28비트 카운터의 오버플로우가 구현되어 있지 않아 어서션 오류로 충돌하게 됩니다. * 재귀 탐지 코드는 재구현되지 않았습니다. 현재로서는 무한 재귀가 그냥 세그폴트(segfault)를 일으킵니다. .. _`report any crash`: https://bitbucket.org/pypy/pypy/issues?status=new&status=open .. __: https://bitbucket.org/pypy/pypy/raw/stmgc-c7/rpython/translator/stm/src_stm/stm/core.h .. _yeokja-doc-9c5a923b8d539612-target-8026: Python 3, CPython, 그리고 기타 ============================== 이 문서에서는 PyPy의 Python 2.7 인터프리터를 기반으로 한 "pypy-stm"에 대해 설명합니다. Python 3 지원에는 반나절 정도의 작업이 필요할 것입니다. 분명히, 제가 말하고자 하는 것은 내일 당장 완성되고 다듬어진 "pypy3-stm" 제품을 가질 수 있다는 뜻이 *아닙니다*. 일반적인 py3k 작업은 아직 이루어지지 않았고, 일반적인 stm 작업도 아직 이루어지지 않았습니다. 하지만 PyPy에서 흔히 그렇듯, 이 둘은 서로 상당히 독립적입니다. 내부 인터페이스가 안정화되어 가는 것으로 보이니, 필요한 반나절의 작업은 조만간 이루어질 것입니다. RPython 프레임워크로 구현된 다른 언어들에도 같은 사실이 적용되지만, 투입해야 할 작업량은 다를 수 있습니다. RPython 내의 STM 프레임워크는 현재 PyPy 인터프리터를 대상으로 하고 있으며, 다른 언어들은 약간 다른 요구 사항을 가질 수 있기 때문입니다. 하지만 일반적으로 지루한 변환 작업은 모두 RPython이 처리하며, 여러분에게는 (바라건대 적은 수의) 어렵고 흥미로운 부분만 남게 됩니다. STM의 핵심은 C로 작성된 라이브러리로 동작합니다(아래의 `구현 세부 사항 참조`_\ 를 참조하세요). 이는 RPython이 만들어내는 것 외의 다른 인터프리터에서도 사용될 수 있음을 의미합니다. Duhton_\ 은 그 초기 예시입니다. 이 시점에서, 이 라이브러리를 CPython용으로 개조하는 것을 생각해볼 수도 있습니다. 다만 미리 경고하자면, 제가 판단하기로는 이는 실패할 수밖에 없는 아이디어입니다. 저는 Duhton을 디버깅하는 데 어려움을 겪었는데, 그것은 CPython보다 무한히 더 단순한데도 그랬습니다. 그것을 무시하더라도, Duhton의 C 소스 코드에서 많은 핵심 설계 결정이 CPython과 다르다는 것을 볼 수 있습니다: 참조 카운팅 없음; 미리 만들어진 "정적(static)" 객체에 대한 제한적인 지원; 도처에 있는 ``stm_read()``\ 와 ``stm_write()`` 매크로 호출(하나라도 빠뜨리면 매우 드물고 매우 알기 어려운 버그가 발생함); 등등. C 언어의 특수 목적 커스텀 확장을 상상해볼 수 있는데, 이를 일반 C로 전처리하는 방식입니다. 제 생각에는 이것이 RPython 자체와 점점 많이 닮아가기 시작하는 것 같지만, 어쩌면 이 접근 방식을 더 선호하실 수도 있습니다. 물론 필요한 모든 C 확장 모듈에 대해 여전히 신경 써야 하지만, 어쩌면 나아갈 길이 생길 수도 있습니다. .. _Duhton: https://bitbucket.org/pypy/duhton 사용자 가이드 ============= .. _yeokja-doc-9c5a923b8d539612-target-10214: 멀티스레드 프로그램 작성법: 10,000피트 상공에서 바라보기 -------------------------------------------------------- PyPy-STM은 멀티스레드 프로그램을 작성하는 두 가지 방법을 제공합니다: * ``thread``\ 나 ``threading`` 모듈을 사용하는 전통적인 방식으로, 처음__\ 에 설명되어 있습니다. * 스레드라는 저수준 개념을 감추는 방법으로, 다음__\ 에서 설명하는 ``TransactionQueue``\ 를 사용합니다. .. __: yeokja-doc-9c5a923b8d539612-target-11603_ .. __: `transaction.TransactionQueue`_ 저수준 스레드의 문제는 잘 알려져 있습니다(특히 GIL 기반 인터프리터가 없는 다른 언어에서 그렇습니다): 메모리 손상, 교착 상태(deadlock), 라이브락(livelock) 등입니다. 스레드를 직접 다루는 OpenMP_\ 와 같은 대안적 접근법들도 있습니다. 이러한 접근법들은 보통 코드에 어떤 구조를 강제합니다. ``TransactionQueue``\ 도 부분적으로는 비슷합니다: 이를 적용하려면 프로그램에 병렬화의 "어느 정도 가능성"이 있어야 합니다. 하지만 저는 ``TransactionQueue``\ 의 적용 범위가 다른 접근법들보다 훨씬 넓다고 생각합니다. 이는 보통 기존 코드의 완전한 재구성을 강제하지 않고도 작동하며, *잠재적*\ 이고 *불완전한* 병렬성을 가진 모든 Python 프로그램에서 작동합니다. 이상적으로는, 최종 프로그래머가 이 병렬성이 어디에 있을 가능성이 높은지 식별하고, 간단한 API를 사용해 이를 시스템에 전달하기만 하면 됩니다. .. _OpenMP: https://en.wikipedia.org/wiki/OpenMP .. _yeokja-doc-9c5a923b8d539612-target-11603: 즉시 교체 가능한 대체품 ----------------------- 멀티스레드 방식의 CPU 집약적인 Python 프로그램은 ``pypy-stm``\ 에서 변경 없이 동작해야 합니다. 여러 CPU 코어를 사용하여 병렬로 실행됩니다. GIL(전역 인터프리터 락)의 기존 의미론은 변경되지 않았습니다: 여러 코어에서 병렬로 실행되지만, ``pypy-stm``\ 은 스레드가 순차적으로 실행되는 것처럼 보이게 하며, 전환은 바이트코드 사이에서만 발생하고 바이트코드 도중에는 발생하지 않습니다. 프로그램은 이를 신뢰할 수 있습니다: ``shared_list.append()/pop()``\ 이나 ``shared_dict.setdefault()``\ 를 동기화 메커니즘으로 사용하는 것은 예상대로 계속 작동합니다. 내부적으로 표준 PyPy나 CPython이 GIL을 해제하는 지점들을 고려하여, 이를 "트랜잭션"의 경계로 대체하는 방식으로 동작합니다. 데이터베이스에서의 트랜잭션과 마찬가지로, 여러 트랜잭션이 병렬로 실행될 수 있지만 어떤 순차적인 순서로 커밋됩니다. 이들은 마치 이 직렬화 순서대로 완전히 실행된 것처럼 동작하는 것으로 보입니다. transaction.TransactionQueue ---------------------------- CPU를 많이 사용하는 프로그램에서는, 어떤 자료 구조에 대한 최외곽 루프나 다른 반복적인 알고리즘을 흔히 쉽게 식별할 수 있으며, 여기서 각 "블록"은 상당한 양의 데이터를 처리하는 것으로 이루어지고, 이 블록들은 서로 독립적일 "가능성이 높습니다". 우리는 그것들이 실제로 독립적이라는 것을 증명할 필요는 없습니다: 그것들이 *자주 독립적*\ 이기만 하면 충분합니다 --- 더 정확히 말하면, 우리가 그것들이 자주 독립적\ *이어야 한다고 생각*\ 하기만 하면 됩니다. 하나의 전형적인 예는 다음과 같으며, 여기서 함수 ``func()``\ 는 일반적으로 많은 양의 코드를 호출합니다:: for key, value in bigdict.items(): func(key, value) 그러면 루프를 다음과 같이 간단히 교체합니다::: from transaction import TransactionQueue tr = TransactionQueue() for key, value in bigdict.items(): tr.add(func, key, value) tr.run() 이 코드의 동작은 동등합니다. 내부적으로 ``TransactionQueue`` 객체는 N개의 스레드를 시작하고 모든 스레드에서 ``func(key, value)`` 호출을 병렬로 실행하려 시도합니다. 하지만 Python보다 저수준인 여러 언어에서 볼 수 있는 일반적인 스레드 풀링 라이브러리와의 차이에 주의하십시오: 함수 호출들이 병렬로 실행된다고 해서 서로 무작위로 뒤섞이는 것은 아닙니다. ``TransactionQueue``\ 를 사용한다고 해서 동작이 바뀌지는 않습니다. 모든 호출은 여전히 어떤 직렬 순서로 실행되는 것처럼 *보입니다*. ``TransactionQueue``\ 의 일반적인 사용법은 다음과 같습니다: 처음에는 성능이 향상되지 않습니다. 사실, 오히려 더 나빠질 가능성이 있습니다. 일반적으로 이는 전체 CPU 사용량이 낮게 유지되는 것으로 나타납니다(N개의 코어보다는 1에 가깝게). JIT 예열 단계에서는 CPU 사용량이 1보다 그리 높아지지 않는 것이 정상이라는 점을 먼저 유의하세요: JIT가 충분히 예열될 기회를 주려면 프로그램을 몇 초 동안, 더 큰 프로그램의 경우 적어도 1분 동안 실행해야 합니다. 하지만 그 이후에도 CPU 사용량이 낮게 유지된다면, ``PYPYSTM`` 환경 변수를 사용하여 무슨 일이 일어나고 있는지 추적할 수 있습니다. ``PYPYSTM=logfile``\ 로 프로그램을 실행하면 ``logfile``\ 이라는 로그 파일이 생성됩니다. 이후 ``pypy/stm/print_stm_log.py`` 유틸리티를 사용하여 이 로그 파일의 내용을 확인합니다. 이는 다음과 같은 출력을 생성합니다(손실된 시간이 큰 순으로 정렬됨):: 10.5s lost in aborts, 1.25s paused (12412x STM_CONTENTION_WRITE_WRITE) File "foo.py", line 10, in f someobj.stuff = 5 File "bar.py", line 20, in g someobj.other = 10 이는 중단된(aborted) 트랜잭션을 실행하는 데 10.5초가 소실되었다는 것을 의미합니다(이는 일시 정지로 인해 추가로 1.25초의 시간 손실을 일으켰습니다). 그 이유는 두 개의 독립된 단일 항목 트레이스백에 나타나 있습니다: 한 스레드는 ``someobj.stuff = 5`` 줄을 실행했고, 다른 스레드는 동시에 같은 객체에 대해 ``someobj.other = 10`` 줄을 실행했습니다. 이 두 개의 쓰기 작업은 같은 객체에 대해 수행됩니다. 이는 충돌을 일으키며, 그 결과 두 트랜잭션 중 하나가 중단됩니다. 위 예제에서 이는 12412번 발생했습니다. 다른 두 가지 충돌 원인은 ``STM_CONTENTION_INEVITABLE``\ 이며, 이는 두 트랜잭션이 모두 출력하기, 소켓에서 읽기, 또는 원시 데이터의 외부 배열에 접근하기와 같은 외부 연산을 시도했음을 의미합니다. 그리고 ``STM_CONTENTION_WRITE_READ``\ 는 한 트랜잭션이 객체에 썼지만 다른 트랜잭션은 그것을 쓰지 않고 단지 읽기만 했음을 의미합니다(이 경우 쓰기를 한 트랜잭션만 보고되며, 읽기의 위치는 기록되지 않는데, 이는 매우 큰 성능 영향 없이는 불가능하기 때문입니다). 충돌의 흔한 원인: * 먼저, I/O나 메모리에 대한 원시적인(raw) 조작은 모두 트랜잭션을 "반드시 중단되어서는 안 되는(inevitable)" 상태로 만듭니다. 언제든 실행 중인 불가피한(inevitable) 트랜잭션은 하나만 있을 수 있습니다. 흔한 경우는 각 트랜잭션이 로그 파일에 데이터를 전송하는 것으로 시작하는 경우입니다. 이런 경우에는 트랜잭션의 끝부분 근처에서 일어나도록 하거나(그러면 대부분 비-불가피(non-inevitable) 모드로 실행될 수 있습니다), 별도의 트랜잭션이나 심지어 별도의 스레드로 위임하도록 리팩터링해야 합니다. * 리스트나 딕셔너리에 쓰는 것은 같은 리스트나 딕셔너리에서 읽는 모든 작업과 충돌하며, 이는 다른 키로 수행된 경우에도 마찬가지입니다. 딕셔너리와 셋의 경우, ``transaction.stmdict``\ 와 ``transaction.stmset`` 타입을 사용해 볼 수 있는데, 이들은 대부분 ``dict``\ 와 ``set``\ 처럼 동작하지만 서로 다른 키에 대한 동시 접근을 허용합니다. (지금까지 이들에게 빠진 것은 지연 반복(lazy iteration)입니다: 예를 들어, ``stmdict.iterkeys()``\ 는 ``iter(stmdict.keys())``\ 로 구현되어 있습니다. 그리고 PyPy의 딕셔너리 및 셋과 달리, STM 버전은 순서가 없습니다.) 키의 동일성(identity)을 사용하는 실험적인 ``stmiddict``\ 와 ``stmidset`` 클래스도 있습니다. * ``time.time()``\ 과 ``time.clock()``\ 은, 나중에 호출된 것처럼 보이는 호출이 실제로 더 큰 숫자를 반환하도록 보장하기 위해 트랜잭션을 불가피(inevitable) 트랜잭션으로 만듭니다. 약간 순서가 어긋난 결과를 얻어도 괜찮다면, ``transaction.time()``\ 이나 ``transaction.clock()``\ 을 사용하십시오. 후자의 연산들은 두 호출이 특정한 순서로 발생했음을 "증명"할 수 있는 경우에만(예를 들어 둘 다 같은 스레드에서 호출된 경우) 증가하는 결과를 반환함을 보장합니다. 그러한 증명이 불가능한 경우에는 무작위로 뒤섞인 값을 얻을 수도 있습니다. (독립적인 두 트랜잭션이 있다면, 일반적으로 둘 중 하나가 다른 하나보다 먼저 완전히 실행된 것처럼 동작합니다. 하지만 ``transaction.time()``\ 을 사용하면 실제로는 그것들이 뒤섞여 있다는 "숨겨진 진실"을 볼 수도 있습니다.) * 클래스 레벨에서 ``transaction.threadlocalproperty``\ 를 사용할 수 있습니다:: class Foo(object): # must be a new-style class! x = transaction.threadlocalproperty() y = transaction.threadlocalproperty(dict) 이는 ``Foo``\ 의 인스턴스가 스레드 로컬인 두 속성 ``x``\ 와 ``y``\ 를 가진다는 것을 선언합니다: 동시에 실행 중인 트랜잭션에서 이를 읽거나 쓰면 서로 독립적인 결과를 반환합니다. (``Foo`` 인스턴스의 다른 모든 속성은 평소처럼 모든 스레드에서 전역적으로 보입니다.) 이는 다음 두 가지 경우에 ``TransactionQueue``\ 와 함께 유용합니다: - 장기간 존재하는 객체(long-lived objects)의 속성 중, 하나의 트랜잭션 동안 값이 변경되지만 트랜잭션 전후로 항상 어떤 초기값으로 재설정되어야 하는 것들을 위한 것입니다(예를 들어, 트랜잭션 시작 시 0으로 초기화되거나, 이 트랜잭션 내에서 처리해야 할 작업 목록으로 사용되는 경우 하나의 트랜잭션이 끝날 때는 항상 비어 있어야 합니다). - 트랜잭션 전반에 걸친 일반적인 캐시를 위한 것입니다. ``TransactionQueue``\ 를 사용하면 고정된 개수 N개의 스레드로 이루어진 풀을 얻게 되며, 각 스레드는 트랜잭션을 순차적으로 실행합니다. 스레드 로컬(thread-local) 속성은 같은 스레드가 마지막으로 저장한 값을 가지며, 이는 임의의 이전 트랜잭션에서 온 것일 수 있습니다. 기본적으로 해당 속성 값의 N개 사본을 얻게 되며, 각 트랜잭션은 그중 임의의 사본에 접근합니다. 캐시에는 잘 작동합니다. 좀 더 자세히 설명하면, ``threadlocalproperty()``\ 의 선택적 인자는 기본값 팩토리입니다: 현재 스레드에서 아직 값이 할당되지 않은 경우, 팩토리가 호출되고 그 결과가 해당 스레드에서의 값이 됩니다(``collections.defaultdict``\ 와 같습니다). 기본값 팩토리가 지정되지 않은 경우, 초기화되지 않은 상태에서 읽으면 ``AttributeError``\ 가 발생합니다. * 위의 모든 내용에 더해, 속성에 같은 값을 반복해서 쓰는 것으로 인해 쓰기-쓰기 충돌이 발생하는 경우도 있습니다. 예를 들어 ea2e519614ab_\ 를 참고하십시오: 이는 객체 필드에 값을 쓰기 전에 이미 썼는지 확인하지 않아 발생하는 두 가지 문제를 수정합니다. ``dont_change_any_more`` 필드는 코드의 그 부분에서 ``True``\ 로 설정되는 플래그이지만, 보통 이 ``rtyper_makekey()`` 메서드는 같은 객체에 대해 여러 번 호출됩니다. 기존 코드는 이 플래그를 반복해서 ``True``\ 로 설정했지만, 이제는 먼저 확인한 후 값이 ``False``\ 일 때만 씁니다. 마찬가지로, 이 커밋의 후반부에서 ``setup_block_entry()`` 메서드는 원래 ``concretetype`` 필드를 설정하는 것과 리스트를 반환하는 것을 모두 수행했지만, 이를 호출하는 두 곳은 서로 달랐습니다: 한쪽은 실제로 ``concretetype`` 필드가 초기화되어야 했던 반면, 다른 쪽은 결과 리스트만 얻으면 되었습니다 --- 이 경우 ``concretetype`` 필드가 이미 설정되어 있든 아니든 상관없었습니다. .. _ea2e519614ab: https://bitbucket.org/pypy/pypy/commits/ea2e519614ab Python은 복잡한 언어라는 점에 유의하시기 바랍니다. 예상하지 못한 곳에서 (어떤 종류든) 충돌을 일으킬 수 있는, 흔치 않은 경우가 다수 존재합니다. 이러한 경우 중 다수는 수정될 수 있습니다. 이해되지 않는 사례가 있다면 보고해 주시기 바랍니다. 원자적 섹션 ----------- 위에서 설명한 ``TransactionQueue`` 클래스는 *원자 섹션(atomic sections),*\ 을 기반으로 하며, 이는 "GIL을 해제하지 않고" 실행하고자 하는 코드 블록입니다. STM 용어로 말하면, 이는 트랜잭션이 중간에 중단되지 않도록 보장하면서 실행되는 코드 블록을 의미합니다. *이는 실험적이며 향후 제거될 수 있습니다*, `Software lock elision`_\ 이 언젠가 구현될 경우에 그렇습니다. 직접적인 사용 예시는 다음과 같습니다:: with transaction.atomic: assert len(lst1) == 10 x = lst1.pop(0) lst1.append(x) 이 예시에서, 리스트의 한쪽 끝에서 꺼낸(pop) 항목이 원자적으로 다른 쪽 끝에 다시 추가된다는 것을 확신할 수 있습니다. 이는 다른 스레드가 특별한 동기화 없이 ``len(lst1)``\ 이나 ``x in lst1``\ 을 실행해도, 항상 같은 결과, 즉 각각 ``10``\ 과 ``True``\ 를 보게 된다는 것을 의미합니다. 그 스레드는 ``lst1``\ 이 원소를 9개만 포함하는 중간 상태를 결코 보지 못합니다. 원자적 섹션(atomic section)은 재진입 가능한 락(lock)과 비슷하지만(중첩될 수 있습니다), 추가로 다른 스레드에서 우연히 같은 락으로 보호되는 코드뿐 아니라 *모든* 코드의 동시 실행으로부터도 보호합니다. 원자적 구역(atomic section)이라는 개념은 매우 강력하다는 점에 유의하십시오. 다음과 같은 코드를 작성하면:: with __pypy__.thread.atomic: time.sleep(10) 그렇다면, 마치 GIL이 있는 것처럼 생각해 보면, GIL을 전혀 해제하지 않은 채 10초짜리 원자적 트랜잭션을 실행하고 있는 셈입니다. 이는 다른 모든 스레드가 전혀 진행하지 못하도록 막습니다. ``pypy-stm``\ 에서는 엄밀히 그렇지는 않지만, 다른 스레드가 언제 진행할 수 있는지에 대한 정확한 규칙은 상당히 복잡하므로, 이런 코드는 결국 다른 모든 스레드를 막게 될 가능성이 높다고 생각해야 합니다. ``atomic``\ 을 실험해보고 싶다면, atomic 블록 바로 앞에 수동으로 트랜잭션 브레이크(break)를 추가해야 할 수도 있다는 점에 유의하십시오. 이는 블록의 경계가 트랜잭션의 경계와 일치한다는 보장이 없기 때문입니다. 후자는 블록만큼 크거나 더 클 수 있습니다. 따라서 큰 atomic 블록을 실행하는 경우, 그 바로 앞에서 트랜잭션을 끊는 것이 좋습니다. 이는 ``transaction.hint_commit_soon()``\ 을 호출하여 수행할 수 있습니다. (이 문제는 추후 수정될 수 있습니다.) 일반 락(lock)과 원자적(atomic) 블록의 상호작용에도 문제가 있습니다. 표준 출력에 대한 ``print``\ 를 포함하여, 락을 가진 파일에 쓸 때 이를 확인할 수 있습니다. 원자적 블록 안에서 실행 중인 한 스레드가 락을 획득하려고 시도하는데 그 시점에 다른 스레드가 같은 락을 가지고 있다면, 전자는 ``thread.error``\ 로 실패할 수 있습니다. (이것에 의존하지 마십시오. 데드락이 발생할 수도 있습니다.) 그 이유는 원자적 블록 안에서 실행되는 동안 어떤 조건이 참이 되기를 "기다리는" 것이 실제로는 의미가 없기 때문입니다. 지금은 예를 들어 모든 print를 ``atomic`` 블록 안에 두거나 아니면 전혀 두지 않도록 하여 이 문제를 우회할 수 있습니다. (이런 종류의 문제는 이론적으로 해결하기 어려우며, 결국 원자적 블록 지원이 제거되는 이유가 될 수도 있습니다.) 락(Locks) --------- **아직 구현되지 않음** thread 모듈의 락은 기본 의미론이 변경되지 않았습니다. 그러나 이를 사용하면(예: ``with my_lock:`` 블록에서와 같이) `Software lock elision`_\ 이라고 불리는 대체 실행 모드가 시작됩니다. 이는 PyPy가 트랜잭션이 락이 해제되는 지점까지 확장되도록 보장하려 시도하며, 성공하면 락의 획득과 해제가 "생략"됨을 의미합니다. 이는 이 경우, 전체 트랜잭션이 기술적으로 락 객체에 어떤 쓰기도 발생시키지 않음을 의미합니다 --- 트랜잭션 이전에도 획득되지 않은 상태였고, 트랜잭션 이후에도 여전히 획득되지 않은 상태입니다. 두 스레드가 같은 락에 대해 ``with my_lock:`` 블록을 실행하는 경우 특히 유용합니다. 각각이 전체 블록을 포함할 만큼 충분히 긴 트랜잭션을 실행한다면, 락에 대한 모든 쓰기는 생략되고 두 트랜잭션은 서로 충돌하지 않습니다. 늘 그렇듯이, 이들은 어떤 순서로 직렬화됩니다: 둘 중 하나가 다른 하나보다 먼저 실행되는 것처럼 보이게 됩니다. 간단히 말하면, 각각은 같은 트랜잭션 안에서 "획득(acquire)" 다음에 "해제(release)"를 실행합니다. 위에서 설명했듯이, 락 상태는 "미획득(unacquired)"에서 "미획득(unacquired)"으로 전이되므로 변경하지 않은 채로 둘 수 있습니다. 이 방식은 우아하게 실패할 수 있습니다: 원자 섹션(atomic section)과 달리, 트랜잭션이 블록 끝까지 실행된다는 보장이 없습니다. 락(lock)을 보유한 상태에서 입출력을 수행하면, 트랜잭션은 입출력 연산 직전에 평소처럼 종료됩니다. 이런 일이 발생하면, 락 생략(lock elision) 모드가 취소되고 락의 "획득됨" 상태가 실제로 기록됩니다. 락이 실제로는 이미 획득되어 있다 하더라도, 트랜잭션이 그 락이 다시 풀릴 때까지 기다릴 필요는 없습니다. 어쨌든 elision 모드로 진입하여 블록의 내용을 잠정적으로 실행할 수 있습니다. 스레드가 멈추는 것은 오직 마지막, 즉 커밋을 시도할 때뿐입니다. 락에 저장된 실제 값이 "미획득(unacquired)" 상태로 다시 바뀌자마자, 이미 실행된 트랜잭션을 계속 진행하여 커밋을 시도할 수 있습니다(이는 평소와 마찬가지로 실패하여 중단되고 처음부터 다시 시작될 수 있습니다). 이 모든 기능은 아직 *구현되지 않았지만,* 여러 개의 잠금을 획득하고 해제하더라도 작동하리라 예상합니다. 생략(elision) 모드 트랜잭션은 처음 획득한 잠금이 해제될 때까지, 또는 코드가 입출력이나 대기 연산을 수행할 때까지(예를 들어 현재 사용 중인 다른 잠금을 기다리는 경우) 확장됩니다. 여러 개의 잠금을 중첩된 순서로 획득하는 일반적인 경우, 이들은 모두 동일한 트랜잭션에 의해 생략됩니다. .. _`software lock elision`: https://www.repository.cam.ac.uk/handle/1810/239410 기타 함수 --------- * 먼저, ``transaction``\ 모듈은 파일 ``lib_pypy/transaction.py``\ 에서 찾을 수 있다는 점에 유의하십시오. 이 파일은 CPython이나 비STM PyPy에서 동일한 프로그램을 실행하기 위해 여기저기 복사될 수 있으며, 폴백(fall-back) 동작을 갖습니다. (동작이 다른 한 가지 경우는 ``atomic``\ 인데, 이 폴백의 경우에는 단순히 일반 락(lock)일 뿐입니다. 따라서 ``with atomic``\ 은 다른 스레드가 다른 ``with atomic``\ 구역에 진입하는 것만 막을 뿐, 다른 스레드가 비원자적(non-atomic) 코드를 실행하는 것은 막지 못합니다.) * ``transaction.getsegmentlimit()``: 이 pypy-stm에 있는 "세그먼트" 개수를 반환합니다. 이는 이 한도를 넘으면 더 많은 스레드가 더 많은 코어에서 실행될 수 없다는 한도입니다. (현재는 세그먼트 간 오버헤드 때문에 4로 제한되어 있지만, 향후 늘어나야 합니다. 또한 설정 가능해야 하며, 기본값은 실제 CPU 개수에 따라 달라져야 합니다.) STM을 사용할 수 없는 경우, 이 함수는 1을 반환합니다. * ``__pypy__.thread.signals_enabled``: 시그널이 활성화된 상태로 코드 블록을 실행하는 컨텍스트 매니저입니다. 기본적으로 시그널은 메인 스레드에서만 활성화되며, 메인 스레드가 아닌 스레드는 시그널을 받지 않습니다(이는 CPython과 같습니다). 메인 스레드가 아닌 스레드에서 시그널을 활성화하는 것은, 스레드가 숨겨져 있어 최종 사용자가 자신의 코드가 메인 스레드 이외의 곳에서 실행되리라고 예상하지 못하는 라이브러리에 유용합니다. * ``pypystm.exclusive_atomic``: ``transaction.atomic``\ 과 유사하지만 중첩되면 오류를 발생시키는 컨텍스트 매니저입니다. * ``transaction.is_atomic()``: 원자적(atomic) 컨텍스트에서 호출된 경우 True를 반환합니다. * ``pypystm.count()``: 호출될 때마다 다른 양의 정수를 반환합니다. 이는 충돌을 생성하지 않고 작동합니다. 반환되는 정수는 대략적으로만 증가하는 순서이며, 이에 의존해서는 안 됩니다. 충돌에 대한 자세한 내용 ----------------------- "Software Transactional Memory"에 기반한 ``pypy-stm`` 해법은 "충돌(conflict)"이 발생하기 쉽습니다. 기본 개념을 다시 말하면, 스레드들은 자신의 코드를 추측적으로(speculatively) 실행하며, 알려진 지점(예: 바이트코드 사이)에서 서로 조율하여 각자의 동작이 어떤 순서로 "커밋(commit)"되어야 하는지, 즉 전역적으로 보이게 되어야 하는지에 합의합니다. 두 커밋 지점 사이의 각 시간 구간을 트랜잭션(transaction)이라고 부릅니다. 충돌은 일관된 순서가 없을 때 발생합니다. 전형적인 예는 두 스레드가 동일한 전역 변수의 값을 동시에 변경하려는 경우입니다. 이 경우, 둘 중 하나만 진행이 허용될 수 있으며, 다른 하나는 일시 중지되거나 중단(트랜잭션을 재시작)되어야 합니다. 이런 일이 너무 자주 발생하면 병렬화가 실패합니다. 멀티스레드 프로그램이 실제로 얼마나 병렬화될 수 있는지는 다소 미묘한 문제입니다. 기본적으로, ``transaction.atomic``\ 을 사용하지 않거나 락을 생략하지 않는 프로그램, 또는 락을 생략하더라도 아주 짧은 시간 동안만 그렇게 하는 프로그램은 (예를 들어 모든 스레드가 그저 같은 전역 카운터를 증가시키기만 하고 다른 일은 하지 않는 것과 같은 인위적인 예시가 아닌 이상) 거의 자유롭게 병렬화됩니다. 그러나 프로그램이 더 긴 트랜잭션을 필요로 하는 경우, 덜 명확한 규칙이 적용됩니다. 정확한 세부 사항 역시 좀 더 안정화되기 전까지는 버전마다 다를 수 있습니다. 개요는 다음과 같습니다. 병렬화는 두 가지 원칙이 지켜지는 한 작동합니다. 첫 번째는 트랜잭션들이 서로 *충돌*\ 하지 않아야 한다는 것입니다. 가장 명백한 충돌 원인은, 모두 전역 공유 카운터를 증가시키는 스레드들이거나, 모두 자신의 계산 결과를 같은 리스트에 저장하는 스레드들입니다 --- 또는 더 미묘하게는, 모두 같은 리스트에서 처리할 작업을 ``pop()``\ 하는 스레드들인데, 이 역시 리스트를 변경하는 것이기 때문입니다. (``transaction.stmdict``\ 를 사용해 이를 우회할 수 있지만, 이 특정 예시의 경우 궁극적으로는 STM을 인식하는 큐가 설계되어야 할 것입니다.) 충돌은 다음과 같이 발생합니다: 트랜잭션이 커밋되면(즉, 성공적으로 완료되면) 아직 진행 중인 다른 트랜잭션이 중단되고 재시도될 수 있습니다. 이는 CPU 시간의 낭비이지만, 최악의 경우에도 GIL보다 나쁘지는 않습니다. 왜냐하면 최소한 하나의 트랜잭션은 성공하기 때문입니다(즉, 최악의 경우 N-1개의 CPU가 무의미한 작업을 하고, 1개의 CPU가 성공적으로 커밋되는 작업을 하게 됩니다). 물론 충돌은 실제로 발생하며, 이를 모두 피하려고 하는 것은 무의미합니다. 예를 들어 워밍업 단계 동안에는 충돌이 많을 수 있습니다. 중요한 것은 전체적으로 충돌을 충분히 드물게 유지하는 것입니다. 또 다른 문제는 오래 실행되는 이른바 "불가피한(inevitable)" 트랜잭션을 피하는 것입니다("불가피한"이라는 말은 "피할 수 없는", 즉 더 이상 중단(abort)될 수 없는 트랜잭션이라는 의미로 사용됩니다). 그런 트랜잭션은 ``atomic``\ 을 사용할 때만, 일반적으로 atomic 블록 내의 I/O 때문에 발생해야 합니다. 이들은 작동하지만, I/O가 수행되기 전에 트랜잭션이 불가피한 상태로 전환됩니다. atomic 블록의 남은 실행 시간 동안, 이들은 병렬 작업을 방해합니다. 가장 좋은 방법은 그러한 연산이 ``atomic`` 바깥에서 완전히 이루어지도록 코드를 구성하는 것입니다. (이는 Twisted에서 블로킹 I/O 작업이 권장되지 않는다는 사실과 무관하지 않으며, 정말로 필요하다면 별도의 스레드에서 수행해야 합니다.) 락 엘리전(lock elision)이 결국 원자적 섹션(atomic section)을 대체하게 될 경우, 오래 실행되는 불가피한 트랜잭션(inevitable transaction)이 생기지는 않겠지만, 같은 문제가 다른 방식으로 발생합니다. 즉, I/O를 수행하면 락 엘리전이 취소되고 락이 실제 락으로 바뀝니다. 이는 다른 스레드들도 이 락을 필요로 하는 경우 커밋을 방해합니다. (락 엘리전이 구현되고 테스트되면 이에 대해 더 자세히 다루겠습니다.) 구현 ==== XXX 이 섹션은 현재 대부분 비어 있습니다 기술 보고서 ----------- STMGC-C7은 `technical report`__\ 에 자세히 설명되어 있습니다. 별도의 `position paper`__\ 는 STM 전반에 대한 저희의 입장을 개괄적으로 설명합니다. .. __: https://bitbucket.org/pypy/extradoc/src/extradoc/talk/dls2014/paper/paper.pdf .. __: https://bitbucket.org/pypy/extradoc/src/extradoc/talk/icooolps2014/ .. _yeokja-doc-9c5a923b8d539612-target-32716: 구현 세부 사항 참조 ------------------- 구현의 핵심은 stmgc_\ 라는 별도의 C 라이브러리에 있으며, c7_ 서브디렉터리(pypy-stm의 현재 버전)와 c8_ 서브디렉터리(최신 개발 버전)에 있습니다. 자세한 내용은 `README.txt`_\ 를 참고하십시오. 특히, 세그먼트(segment)라는 개념이 거기서 설명됩니다. .. _stmgc: https://bitbucket.org/pypy/stmgc/src/default/ .. _c7: https://bitbucket.org/pypy/stmgc/src/default/c7/ .. _c8: https://bitbucket.org/pypy/stmgc/src/default/c8/ .. _`README.txt`: https://bitbucket.org/pypy/stmgc/raw/default/c7/README.txt PyPy 자체는 그 위에 read__ 및 write__ 배리어와 `"becomes-inevitable-now" 배리어`__\ 의 자동 배치, `RPython 변환(transformation)으로서의 트랜잭션 시작/중지`__ 및 `지원`__ `C 코드`__\ 로서의 로직, 그리고 JIT에서의 지원(주로 `트레이스에 대한 변환(transformation) 단계`__ 및 `assembler.py`__\ 에서의 커스텀 어셈블리 생성으로서)을 추가합니다. .. __: https://bitbucket.org/pypy/pypy/raw/stmgc-c7/rpython/translator/stm/readbarrier.py .. __: https://bitbucket.org/pypy/pypy/raw/stmgc-c7/rpython/memory/gctransform/stmframework.py .. __: https://bitbucket.org/pypy/pypy/raw/stmgc-c7/rpython/translator/stm/inevitable.py .. __: https://bitbucket.org/pypy/pypy/raw/stmgc-c7/rpython/translator/stm/jitdriver.py .. __: https://bitbucket.org/pypy/pypy/raw/stmgc-c7/rpython/translator/stm/src_stm/stmgcintf.h .. __: https://bitbucket.org/pypy/pypy/raw/stmgc-c7/rpython/translator/stm/src_stm/stmgcintf.c .. __: https://bitbucket.org/pypy/pypy/raw/stmgc-c7/rpython/jit/backend/llsupport/stmrewrite.py .. __: https://bitbucket.org/pypy/pypy/raw/stmgc-c7/rpython/jit/backend/x86/assembler.py 참고 사항 ========= 참고: https://bitbucket.org/pypy/pypy/raw/default/pypy/doc/project-ideas.rst (STM 관련 절).