본문 바로가기

소프트웨어 이야기/프로그래밍 언어와 프레임워크

(Django) Django에서 Thread를 다룰 때 주의할 점

💡한줄 요약

Django 스크립트에서 Thread를 사용하는 경우, Thread가 종료될 때 명시적으로 DB Connection을 닫아주는 것이 좋다. 

 

Djagno에서 Thread를 사용할 때, DB Connection이 관리되는 방식

Django에서 DB에 접근하는 Thread를 사용하는 경우, Thread마다 새로운 DB Connection을 생성된다.

Django는 request가 종료될때, request_finished signal을 발생시켜 지난 DB Connection들을 모두 닫는다. ( 참고 - DB Connection을 관리하는 방법 )

즉, Thread에서 만들어진 DB Connnecion은 Thread를 실행시킨 메인 프로세스가 종료될 때 닫힌다. 

 

import threading
from product.models import Product

def print_product(index):
    print(f"{index} function call")
    print(Product.objects.last().name)

thread_list = []

for i in range(30):
    th = threading.Thread(target=fun, name="[th def {}]".format(i), args=(i,))
    th.start()
    thread_list.append(th)

django console에서 위의 함수를 실행해보면, 콘솔이 종료 될 때까지 커넥션이 닫히지 않고 남아있는 것을 확인할 수 있다.

 

 

MultiThread와 DB Connection

Thread Pool을 사용하는 경우, Connection을 재사용한다. ( Thread 자체가 재사용된다. 그래서 Connection도 재사용된다. )

import concurrent.futures
from video.models import Product

def print_product(index):
    print(Product.objects.last())

with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
    for i in range(100):
        future = executor.submit(print_product, (i))

그래서 위와 같이 함수를 100번 실행하더라도, 이 경우 max_workers 갯수만큼만 커넥션이 생성된다. 

import concurrent.futures
from video.models import Product

def print_product(index):
    print(Product.objects.last())

with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
    for i in range(100):
        future = executor.submit(print_product, (i))

with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
    for i in range(100):
        future = executor.submit(print_product, (i))

그러나 위와 같이 ThreadPoolExecutor를 각각 실행한 경우, 총 6개의 DB Coonection이 새로 생성된다. 

 

DB Connection 종료 시점

이러한 원리 때문에 실행시간이 긴 스크립트에서 멀티쓰레드를 사용하면, 다량으로 만들어진 Connection이 닫히지 않고 대기하는 상황들이 발생한다.

그러면 DB Connection이 계속 점유되고, Connection 수가 급증하는 현상이 발생할 수 있다. 

 

때문에 Thread가 다 사용되고 나면, Connection을 명시적으로 닫아주는 것이 좋다. 

import concurrent.futures
from video.models import Product
from django import db


def print_product(index):
    print(Product.objects.last())
    db.connections.close_all()


with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
    for i in range(100):
        future = executor.submit(print_product, (i))

 

참고

https://stackoverflow.com/questions/8242837/django-multiprocessing-and-database-connections

https://stackoverflow.com/questions/20058589/closing-db-connection-with-djangos-persistent-connection-in-a-multi-threaded-sc

https://stackoverflow.com/questions/1303654/threaded-django-task-doesnt-automatically-handle-transactions-or-db-connections