47

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
2
  • 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. Commented Aug 24, 2013 at 17:27
  • 11
    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. Commented Aug 24, 2013 at 18:02

4 Answers 4

73

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()
Sign up to request clarification or add additional context in comments.

9 Comments

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
Note that this will leak database connections, as django will create a new db connection per thread and you are responsible for closing it.
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
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.
@spookylukey what are the implications if it is simply aborted?
|
20

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

Comments

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

Comments

3

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.