9

As per the title, I am wondering if Django, when run via WSGI or Gunicorn, uses one thread to process several requests?

I know it is a bad practice to access the request from places not meant to be accessed from, but I still want to do it. (for good reasons in my opinion, such as accessing the current user and site in my custom template loader. Django template loader doesn't offer these two entities.)

Now I managed to get access to the user, etc. from everywhere using the following bit of code:

import threading

_thread_locals = threading.local()

def get_current_request():
    return getattr(_thread_locals, "request", None)

class RequestThreadMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        _thread_locals.request = request
        response = self.get_response(request)
        return response

My question/concern is: is it guaranteed that requests/users don't get mixed up in production? I am concerned the second user, for instance, gets identified as the first user because the thread is being used for my than one request. Or, the request gets overwritten by another request while the original request hasn't finished yet. Is that possible, or every request has its own local thread in WSGI and Gunicorn?

Adam
  • 2,948
  • 10
  • 43
  • 74
  • Btw you can access `user` and `request` context variables in Django template by default, for insatnce: `{{ user }}`, `{{ request.user }}`, `{{ request.get_host }}` and use [get_current_site](https://docs.djangoproject.com/en/3.1/ref/contrib/sites/#get-current-site-shortcut) to get the current site if `request.get_host` is not enough. – HTF Mar 25 '21 at 09:55
  • @HTF I am talking about this django/template/loader/ -- not .html files – Adam Mar 25 '21 at 15:04

5 Answers5

20

Consider here Gunicorn as a web server. It has master and workers processes. Master process selects free worker process for handling http request. There are 3 type of workers:

  • synchronous (sync)
  • synchronous with threads (worker shares memory for running threads)
  • asynchronous (async)

Each sync worker without threads handles single request at a time, consider we are having 2 requests they either would be handled by 2 different workers (separate and isolated from each other python processes) or same worker but sequentially or parallely by one worker if it has more then one thread To run an app with a sync workers without threads run it as follows:

gunicorn --workers=3 main:app

To run an app with a sync workers and with a threads:

gunicorn --workers=3 --threads=2 --worker-class=gthread main:app 

In above example each of the worker could handle 2 requests at a time.

With async worker we would have request concurrency: each worker (python process) would process many requests at a time but by one process. To run gunicorn with async workers your should properly set worker class:

gunicorn --worker-class=gevent --worker-connections=1000 --workers=3 main:app

It could be fully quarantied that request don't get mixed if you choose sync workers without threads and it seems that async too, for sync workers with threads your should to implement thread synchronization where multiple threads could write simultaneously.

Michael Ushakov
  • 1,639
  • 1
  • 10
  • 18
  • 1
    Thanks for the explanation. If I understand correctly, if I use `gunicorn --workers=3 main:app` without passing the `--threads` parameter no matter how many concurrent requests there are they won't get mixed up? Is that correct? – Adam Mar 24 '21 at 15:32
  • 1
    @Adam Silver, Yes – Michael Ushakov Mar 24 '21 at 16:14
3

In theory, Django uses asynchronous instances to process different requests. That is as the request from a client comes to I/O operation, it will send the I/O request (i.e. look up in database) and then store the current status and let another client process its request. Thus, you need not worry "thread safe" or "thread data safe" issue. Because there is always a master thread in Django that takes care of production threads.

To check if Django actually runs multiple threads at one time, install and use htop on your Linux machine.

George Y
  • 525
  • 3
  • 14
3

It is safe to use threading.local with blocking functions (that's why they called blocking after all), but better use contextvars to isolate cooperative tasks as well.

madbird
  • 1,326
  • 7
  • 11
3

Watch how the django-simple-history works (maybe this library can meet your demands). You can use:

try:
    from asgiref.local import Local as LocalContext
except ImportError:
    from threading import local as LocalContext

and then the variable LocalContext() is thread safe.

Here is an example.

ramwin
  • 5,803
  • 3
  • 27
  • 29
1

As far as I know, Django does not depends on multi-threading.

Each WSGI or Gunicorn runner-thread runs only one Django instance that deals with only one request.

You can check that with htop or better with lsof

Ouss
  • 2,912
  • 2
  • 25
  • 45