Multithreading for Python Django

40,941

Solution 1

I've continued using this implementation at scale and in production with no issues.

Decorator definition:

def start_new_thread(function):
    def decorator(*args, **kwargs):
        t = Thread(target = function, args=args, kwargs=kwargs)
        t.daemon = True
        t.start()
    return decorator

Example usage:

@start_new_thread
def foo():
  #do stuff

Over time, the stack has updated and transitioned without fail.

Originally Python 2.4.7, Django 1.4, Gunicorn 0.17.2, now Python 3.6, Django 2.1, Waitress 1.1.

If you are using any database transactions, Django will create a new connection and this needs to be manually closed:

from django.db import connection

@postpone
def foo():
  #do stuff
  connection.close()

Solution 2

Celery is an asynchronous task queue/job queue. It's well documented and perfect for what you need. I suggest you start here

Solution 3

The most common way to do asynchronous processing in Django is to use Celery and django-celery.

Solution 4

tomcounsell's approach works well if there are not too many incoming jobs. If many long-lasting jobs are run in short period of time, therefore spawning a lot of threads, the main process will suffer. In this case, you can use a thread pool with a coroutine,

# in my_utils.py

from concurrent.futures import ThreadPoolExecutor

MAX_THREADS = 10


def run_thread_pool():
    """
    Note that this is not a normal function, but a coroutine.
    All jobs are enqueued first before executed and there can be
    no more than 10 threads that run at any time point.
    """
    with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
        while True:
            func, args, kwargs = yield
            executor.submit(func, *args, **kwargs)


pool_wrapper = run_thread_pool()

# Advance the coroutine to the first yield (priming)
next(pool_wrapper)
from my_utils import pool_wrapper

def job(*args, **kwargs):
    # do something

def handle(request):
    # make args and kwargs
    pool_wrapper.send((job, args, kwargs))
    # return a response
Share:
40,941
tomcounsell
Author by

tomcounsell

to get in touch contact Tom Counsell on LinkedIn.com/in/tomcounsell and follow on GitHub.com/tomcounsell

Updated on July 09, 2022

Comments

  • tomcounsell
    tomcounsell almost 2 years

    Some functions should run asynchronously on the web server. Sending emails or data post-processing are typical use cases.

    What is the best (or most pythonic) way write a decorator function to run a function asynchronously?

    My setup is a common one: Python, Django, Gunicorn or Waitress, AWS EC2 standard Linux

    For example, here's a start:

    from threading import Thread
    
    def postpone(function):
        def decorator(*args, **kwargs):
            t = Thread(target = function, args=args, kwargs=kwargs)
            t.daemon = True
            t.start()
        return decorator
    

    desired usage:

    @postpone
    def foo():
        pass #do stuff
    
    • Guillaume Vincent
      Guillaume Vincent almost 11 years
      Look at this post too stackoverflow.com/questions/573618/…. For a scheduled Job choose a cron based solution. Scheduled Job, Asynchronous tasks choose Celery. I start by github.com/tivix/django-cron before migrate to Celery recently.
    • tomcounsell
      tomcounsell almost 11 years
      Thanks for all the answers so far, however Celery requires quite a bit of overhead (installing the app, creating a db for it). So while Celery is a solution, it doesn't answer my question about writing a standalone decorator to multithread a function.
  • Deepak
    Deepak about 9 years
    I am also running this same implementation in production with no issues and the best part is that it works with uwsgi without any any major performance issues
  • CadentOrange
    CadentOrange over 8 years
    Do you limit the number of threads spawned? Is it possible for someone to thread bomb your code?
  • tomcounsell
    tomcounsell over 8 years
    I have not used this in scenarios where more than just a couple threads can be created in a single web request. I have no reason to throw a function that uses this decorator inside a loop. It'd be better to simply wrap that logic into a single function that runs in one additional thread. The goal here is to postpone any processing that is not vital to returning the web request.
  • Chronial
    Chronial over 7 years
    Note that this will leak database connections, as django will create a new db connection per thread and you are responsible for closing it.
  • oden
    oden over 7 years
    Testing confirms that @Chronial is correct. If your function performs a database transaction (read is what I tested) then a new connection is create. Then the thread terminates the connection remains and after 80 Postgres rejects any further connections from being created
  • Algorithmatic
    Algorithmatic over 7 years
    Would it be safe to use this when ur view functions has a couple of myModel.Objects.create()?
  • ZaneLanski
    ZaneLanski almost 7 years
    When the server shuts down and your postponed function has not run, or is part way through running, it will simply be aborted. You need to call the join method of the new thread from the main thread, and there isn't a good way to do that. This is one of the reasons why people use Celery etc.
  • Neil
    Neil over 3 years
    @spookylukey what are the implications if it is simply aborted?
  • ZaneLanski
    ZaneLanski over 3 years
    @Neil - that depends entirely on the job and what it does - whether it was important work, whether it uses database transactions or not, whether it will leave something in an inconsistent state, whether you have a mechanism to detect that and retry without issues etc.