동시성 코드에서 컨텍스트 관리 - 1

김재영·2023년 3월 28일
1

ref: https://docs.python.org/3/library/contextvars.html

Python DJANGO 프레임워크에서 asgi application을 tracing 하는 과정에서 겪은 이슈입니다.

WSGI 에서 threading.local

기존의 wsgi application는 기본적으로 1개의 request가 들어올 때마다 한개의 스레드가 소진되기 때문에 threading.local 을 이용하면 변수의 범위를 관리할 수 있습니다. 예로들어 멀티스레딩 상황에서 각 스레드 별 침해받지 않는 고유한 스토리지가 필요할 때를 가정해봅니다. 로컬리티가 thread에 바인딩 되어 있는 변수가 있다면 다른스레드와 독립적으로 스토리지를 관리할 수 있습니다. 즉, 멀티스레딩 상황에서 threading.local 을 이용하면 예상치 못하게 다른 스레딩의 코드에 의해 변수가 침해받는 일이 일어나지 않습니다.

ASGI 에서의 threading.local

asgi application 은 single thread 기반으로 하나의 스레드 안에서 여러개의 task 가 concurrent 하게 돌아갑니다. wsgi 가 하나의 Task 에 헌신적이였다면 asgi 에서는 await 을 이용하여 다른 task 또한 바라 볼 수 있게 해줍니다. 이러한 방식은 thread 사용에 있어서 효율적입니다. 하나의 작업에 한개의 스레드를 소진 하는 대신 스레드 소진없이 여러 작업을 concurrency 하게 처리하여 thread exhaustion 과 같은 현상을 wsgi 비해 줄일 수 있습니다.

ASGI 에서의 threading.local 한계

하지만 이 동시성으로 인해 기존의 방법(thread-local)으로는 context 관리가 어려지게 됩니다. 한개의 thread 에서 여러개의 코드가 동시에 실행되며 thread-local 값은 기존의 wsgi 에서와 같이 로컬리티를 보장받을 수 없습니다. 즉,thread에 바인딩 되어있는 변수는 동시성 코드에 의해 현재값에 대한 context 관리가 어려워지게 됩니다.

동시성 코드에서 콘텍스트 관리

aysnc 에 진행되는 프로세스에서 현재 컨텍스트를 관리하기 위해서 contextvars 라는 새로운 개념이 등장합니다. 로컬리티가 스레드에 바인딩 되던 thread-local 변수와 달리 현재 컨텍스트에 대한 manipulation 이 가능한 새로운 컨텍스트 관리 메커니즘이 등장합니다. 저는 threading.local 과 contextvars 쓰임에 대한 차이를 우주 평행세계에 빗대어 이해해 봤습니다.

나라는 사람(1명)은 A, B, C, D, E 라는 5개의 평행세계를 살 수 있습니다.
sync(wsgi) 의 방식에서 나는 평행세계마다 독립적인 thread-local 공간을 가지고 있습니다. 각각은 자신의 존재와 정체성에 대한 기억을 유지하기 위해서 thread-local 이라는 공간을 이용하여 데이터를 보관합니다.

async(asgi) 에서도 동일하게 5개의 평행세계가 있지만 이 평행세계는 하나의 thread 에서 만들어져 thread-local 이라는 공간을 서로 공유하게 됩니다.
서로 자신만의 고유한 무언가를 이 장소에 보관하게 되면 어떻게 될까요? A가 보관했다고 생각했던 표식이 C에 의해 침해받았다면 A는 자신이 C라고 생각하게 될것입니다. 이러한 문제는 우리가 누구인지에대한 추적을 불가능하게 만들 수 있습니다.

나는 다른 평행세계의 나에 의해 침해받을 수 있는 이 thread-local 이라는 저장소를 버리고자 합니다. 대신 내가 누군지 알기 위해서 사진을 찍습니다. 이 사진은 매우 특별한 기능이 있습니다.

  • 내가 표시해둔 표식을 모두 보여줍니다. 이것은 사진을 찍는 현재 시점의 내가 어떤 평행세계에 있는지 기억을 회상할 수 있게 하는 표식입니다.

  • 언제든지 사진을 바라보면 사진을 찍은 평행세계로 들어갑니다. 들어가서 표식의 값을 변경할 수 있습니다. 사진으로 진입한 시점부터는 어떠한 변화(표식에대한)도 사진속 세상에서만 영향을 미칩니다.

이것을 코드로 나타내보면 다음과 같습니다.

import contextvars
import asyncio

## who_am_i 표식이 만들어짐
who_am_i = contextvars.ContextVar("who_am_i")


def do_something(some):
    who_am_i.set(some)

async def universeA():
    ## 표식에 universeA 남기기
    who_am_i.set("universeA")

    ## 사진찍기-copy_context
    context = contextvars.copy_context()

    print(f"in universeA:{list(context.items())}")

    ## 사진찍은 시점으로 들어가기, universeA 를 destroy
    context.run(do_something, "destory universeA")

    print(f"after destory context-inner who_am_i:{context.get(who_am_i)}")
    print(f"after destory context-outer who_am_i:{who_am_i.get()}")


async def universeB():
    who_am_i.set("universeB")
    context = contextvars.copy_context()
    print(f"in universeB:{list(context.items())}")

async def universeC():
    who_am_i.set("universeC")
    context = contextvars.copy_context()
    print(f"in universeC:{list(context.items())}")


async def universeD():
    who_am_i.set("universeD")
    context = contextvars.copy_context()
    print(f"in universeD:{list(context.items())}")


async def universeE():
    who_am_i.set("universeE")
    context = contextvars.copy_context()
    print(f"in universeE:{list(context.items())}")

async def cross_universe():
    a = asyncio.create_task(universeA())
    b = asyncio.create_task(universeB())
    c = asyncio.create_task(universeC())
    d = asyncio.create_task(universeD())
    e = asyncio.create_task(universeE())
    await asyncio.gather(a, b, c, d, e)

if __name__ == "__main__":
    asyncio.run(cross_universe())

contextvars 는 단일 스레드 concurrency 작업에 대해 서로 침해받지 않는 독립적인 데이터 storage 를 제공해 줍니다. 평행세계가 만들어져 분화된 시점으로 각각은 서로 다른 표식을 가지게되고 이 표식은 각각의 평행세계마다 다른 값을 가질 수 있습니다. 이것이 파이썬이 말하는 현재 컨텍스트 라는 개념이며 coroutine 에서의 로컬리티를 보장하는 방법입니다.

contextvars는 단 몇줄의 코드 추가로 동시성코드에서 현재 컨텍스트에 대한 관리 메카니즘을 제공해줍니다. thread-local 과의 backward-compatibilty 를 제공하고 있으며 python3.7 부터 적용가능합니다. 컨텍스트를 manipulation 한다는 점에서 프로그래머 입장에서 신경써야 할 부분이 늘어나고 혼란을 줄 수 있지만 사용하면서 너무 흥미롭고 마법같은 기술이라고 생각하게 됐습니다. 다음 포스팅에서는 django 어플리케이션에서 실제로 이 메카니즘을 어떻게 사용하고 있는지 제가 이해한 부분을 간략한 코드와 함께 설명해보겠습니다.

0개의 댓글