코드로 우주평화
Python threading.local 와 ContextVar 비교 본문
threading.local
와 ContextVar
는 둘 다 데이터를 격리하고 동적으로 할당한다는 점에서 유사한 목적을 가지고 있지만 사용되는 상황과 특징이 조금 다르다.
이 글에서는 둘의 공통점과 차이점을 예제 코드와 함께 비교해보고자 한다.
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
ContextVar
도 threading.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.local
와 ContextVar
는 어떤 차이점이 있을까?
둘의 차이점을 더 자세히 살펴보자.
차이점
threading.local
은 스레드에 특화되어 있고, ContextVar
는 코루틴에 더 적합하다.
즉, 멀티 스레딩 환경에서는 ContextVar
와 threading.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.local
와 ContextVar
비교해보았다. 둘의 쓰임새는 비슷하지만 동작환경에 따라 결과가 달라진다는 것을 유념하여 적절하게 사용하기 바란다.
'나는 이렇게 학습한다 > Language' 카테고리의 다른 글
Python _ setup.py 의 역할과 사용법 (0) | 2023.08.20 |
---|---|
Python _ Decimal 모듈로 부동 소수점 문제 해결하기 (2) | 2023.05.16 |
JavaScript _ 콜백 함수를 알아보자 (0) | 2023.05.09 |
Python _ zoneinfo 사용법, ZoneInfo 와 pytz 차이 (0) | 2022.10.27 |
Python _ Protocol로 인터페이스 만드는 방법 (1) | 2022.08.16 |