나는 이렇게 학습한다/Language

Python threading.local 와 ContextVar 비교

daco2020 2023. 9. 26. 17:15

threading.localContextVar 는 둘 다 데이터를 격리하고 동적으로 할당한다는 점에서 유사한 목적을 가지고 있지만 사용되는 상황과 특징이 조금 다르다.

 

이 글에서는 둘의 공통점과 차이점을 예제 코드와 함께 비교해보고자 한다.

 

 

 

threading.local

threading.local 은 각 스레드마다 고유한 데이터를 가질 수 있게 해준다.

 

아래 예제 코드를 살펴보자.

import threading

# threading.local 객체 생성
thread_local_data = threading.local()


def 스레드_울음소리():
    thread_local_data.value = "끼룩끼룩!"


def 스레드_동물호출():
    value = getattr(thread_local_data, "value", "...!")
    print(f"스레드에서 호랑이가 {value}")


# 실행
def 스레드_실행():
    스레드_울음소리()
    스레드_동물호출()


thread = threading.Thread(target=스레드_실행)
thread.start()
thread.join()

# 출력: '스레드에서 호랑이가 끼룩끼룩!'

thread_local_data 라는 스레드내 변수를 선언하고 스레드_울음소리() 함수를 통해 value 를 저장한다.

 

이후 스레드_동물호출() 함수의 getattr(thread_local_data, "value", "...!") 코드를 통해 변수에 저장되어 있던 value 값을 가져온다.

 

이렇게 하면 함수에 인자를 주입하거나 전역변수를 설정하지 않고도, 동일 스레드내 어디서든 thread_local_data 변수를 가져와 사용할 수 있게 된다.

 

 

 

ContextVar

ContextVarthreading.local 과 유사하게 컨텍스트별 고유한 데이터를 관리하는데 사용한다.

 

아래 예제 코드를 살펴보자.

from contextvars import ContextVar

# ContextVar 객체 생성, threading.local() 와 달리 default 를 지정할 수 있다.
context_var = ContextVar("context_var", default="...!") 


def 컨텍스트_울음소리():
    context_var.set("왈왈!")


def 컨텍스트_동물호출():
    value = context_var.get()
    print(f"컨텍스트에서 토끼가 {value}")


# 실행
def 컨텍스트_실행():
    컨텍스트_울음소리()
    컨텍스트_동물호출()


컨텍스트_실행()

# 출력: '컨텍스트에서 토끼가 왈왈!'

context_var 라는 컨텍스트내 변수를 선언하고 컨텍스트_울음소리() 함수를 통해 값을 set 한다.

 

이후 컨텍스트_동물호출() 함수의 context_var.get() 코드를 통해 변수에 저장되어 있던 value 값을 가져와 사용할 수 있다.

 

이처럼 둘은 동작과 결과가 유사하다. 그렇다면 threading.localContextVar 는 어떤 차이점이 있을까?

 

둘의 차이점을 더 자세히 살펴보자.

 

 

 

차이점

threading.local은 스레드에 특화되어 있고, ContextVar는 코루틴에 더 적합하다.

 

즉, 멀티 스레딩 환경에서는 ContextVarthreading.local이 비슷하게 동작하지만, 싱글 스레드 환경에서의 동시성을 다루는 코루틴에서는 ContextVar 를 선택해야 한다.

 

아래는 threading.local 을 비동기로 실행시킨 예제 코드이다.

import asyncio
import threading

thread_local_data = threading.local()


async def 동물호출(number: str) -> None:
    await asyncio.sleep(0.1)
    print(f"{number} 푸바오가 {getattr(thread_local_data, 'value', '...!')}")


async def 울음소리1번() -> None:
    thread_local_data.value = "야옹!"
    print(f"1번 울음소리: {thread_local_data.value}")
    await 동물호출("1번")


async def 울음소리2번() -> None:
    thread_local_data.value = "삐리삐리뽀!"
    print(f"2번 울음소리: {thread_local_data.value}")
    await 동물호출("2번")


async def main() -> None:
    await asyncio.gather(울음소리1번(), 울음소리2번())


asyncio.run(main())

#  1번 울음소리: 야옹!
#  2번 울음소리: 삐리삐리뽀!
#  1번 푸바오가 삐리삐리뽀!
#  2번 푸바오가 삐리삐리뽀!

출력 결과를 보면, 1번 울음소리가 야옹! 이므로 1번 푸바오가 야옹! 이라는 결과가 나와야 하지만 1번 푸바오도 삐리삐리뽀! 라고 우는 것을 볼 수 있다.

 

문제가 발생한 이유는 threading.local은 멀티스레딩 환경에서 각 스레드마다 고유한 값을 갖도록 설계된 거지만, 여기서는 모든 비동기 함수가 하나의 스레드에서 실행되고 있기 때문이다.

 

즉, 울음소리1번울음소리2번이 동시에 실행되면서 thread_local_data.value 값이 삐리삐리뽀!로 덮어쓰여진 것이다.

 

 

그렇다면 ContextVar 는 어떨까? 다음은 ContextVar 를 비동기로 실행시킨 예제 코드이다.

import asyncio
from contextvars import ContextVar

from contextvars import ContextVar

context_var_data: ContextVar[str] = ContextVar("context_var_data")


async def 동물호출(number: str) -> None:
    await asyncio.sleep(0.1)
    print(f"{number} 푸바오가 {context_var_data.get('...!')}")


async def 울음소리1번() -> None:
    context_var_data.set("야옹!")
    print(f"1번 울음소리: {context_var_data.get()}")
    await 동물호출("1번")


async def 울음소리2번() -> None:
    context_var_data.set("삐리삐리뽀!")
    print(f"2번 울음소리: {context_var_data.get()}")
    await 동물호출("2번")


async def main() -> None:
    await asyncio.gather(울음소리1번(), 울음소리2번())


asyncio.run(main())

# 1번 울음소리: 야옹!
# 2번 울음소리: 삐리삐리뽀!
# 1번 푸바오가 야옹!
# 2번 푸바오가 삐리삐리뽀!

울음소리1번에서 설정한 "야옹!"과 울음소리2번에서 설정한 "삐리삐리뽀!"가 1번, 2번 푸바오 각각에게 할당되어 잘 출력 되고 있다.

 

이처럼 ContextVar 로 코드를 실행했을 때에는 각각의 비동기 함수에서 설정한 컨텍스트 변수 값이 안전하게 유지되고 있는 것을 볼 수 있다.

 

이를 간단하게 도식화해보면 다음과 같다.

그림의 스레드 1 처럼 복수 컨텍스트가 동작하는 비동기 환경에서 threading.local 을 사용한다면 컨텍스트 1컨텍스트 2 의 데이터 격리를 보장하지 못하고 영향을 미치게 된다.

 

반면, 단일 컨텍스트를 사용하는 스레드 2 에서는 하나의 컨텍스트만을 가지므로 threading.local 을 사용하거나 ContextVar 를 사용하거나 둘 다 동일한 결과를 기대할 수 있다.

 

 

이상으로 threading.localContextVar 비교해보았다. 둘의 쓰임새는 비슷하지만 동작환경에 따라 결과가 달라진다는 것을 유념하여 적절하게 사용하기 바란다.