Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Making thread_sensitive=True the default
This makes thread_sensitive=True the default behaviour for SyncToAsync.

It's easy to run afoul of thread-sensitive code when dealing with
databases or frameworks like Django, and while the penalty for having
this on by default is just slightly worse performance, the penalty for
having it off is very confusing bugs all over the place - so, to keep
with the Django culture of safety first, it's best to default this on.

Additionally, this allows thread_sensitive to be passed into the
`sync_to_async` decorator, so if people are sure their code is safe and
want to run it in its own thread for performance reasons, they can use
`@sync_to_async(thread_sensitive=False)`.
  • Loading branch information
andrewgodwin committed Sep 11, 2020
1 parent 66a6e68 commit 7becc9d
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 18 deletions.
6 changes: 5 additions & 1 deletion README.rst
Expand Up @@ -43,7 +43,11 @@ ASGI server.

Note that exactly what threads things run in is very specific, and aimed to
keep maximum compatibility with old synchronous code. See
"Synchronous code & Threads" below for a full explanation.
"Synchronous code & Threads" below for a full explanation. By default,
``sync_to_async`` will run all synchronous code in the program in the same
thread for safety reasons; you can disable this for more performance with
``@sync_to_async(thread_sensitive=False)``, but make sure that your code does
not rely on anything bound to threads (like database connections) when you do.


Threadlocal replacement
Expand Down
11 changes: 8 additions & 3 deletions asgiref/sync.py
Expand Up @@ -255,7 +255,7 @@ class SyncToAsync:
# Single-thread executor for thread-sensitive code
single_thread_executor = ThreadPoolExecutor(max_workers=1)

def __init__(self, func, thread_sensitive=False):
def __init__(self, func, thread_sensitive=True):
self.func = func
functools.update_wrapper(self, func)
self._thread_sensitive = thread_sensitive
Expand Down Expand Up @@ -365,6 +365,11 @@ def get_current_task():
return None


# Lowercase is more sensible for most things
sync_to_async = SyncToAsync
# Lowercase aliases (and decorator friendliness)
async_to_sync = AsyncToSync


def sync_to_async(func=None, thread_sensitive=True):
if func is None:
return lambda f: SyncToAsync(f, thread_sensitive=thread_sensitive)
return SyncToAsync(func, thread_sensitive=thread_sensitive)
45 changes: 31 additions & 14 deletions tests/test_sync.py
@@ -1,4 +1,5 @@
import asyncio
from concurrent.futures import thread
import threading
import time
import multiprocessing
Expand Down Expand Up @@ -280,11 +281,10 @@ async def middle():
await inner()

# Inner sync function
@sync_to_async
def inner():
result["thread"] = threading.current_thread()

inner = sync_to_async(inner, thread_sensitive=True)

# Run it
middle()
assert result["thread"] == threading.current_thread()
Expand All @@ -301,22 +301,20 @@ async def test_thread_sensitive_outside_async():
result_2 = {}

# Outer sync function
@sync_to_async
def outer(result):
middle(result)

outer = sync_to_async(outer, thread_sensitive=True)

# Middle async function
@async_to_sync
async def middle(result):
await inner(result)

# Inner sync function
@sync_to_async
def inner(result):
result["thread"] = threading.current_thread()

inner = sync_to_async(inner, thread_sensitive=True)

# Run it (in supposed parallel!)
await asyncio.wait([outer(result_1), inner(result_2)])

Expand All @@ -339,22 +337,20 @@ async def level1():
await level2()

# Sync level 2
@sync_to_async
def level2():
level3()

level2 = sync_to_async(level2, thread_sensitive=True)

# Async level 3
@async_to_sync
async def level3():
await level4()

# Sync level 2
@sync_to_async
def level4():
result["thread"] = threading.current_thread()

level4 = sync_to_async(level4, thread_sensitive=True)

# Run it
level1()
assert result["thread"] == threading.current_thread()
Expand All @@ -370,22 +366,20 @@ async def test_thread_sensitive_double_nested_async():
result = {}

# Sync level 1
@sync_to_async
def level1():
level2()

level1 = sync_to_async(level1, thread_sensitive=True)

# Async level 2
@async_to_sync
async def level2():
await level3()

# Sync level 3
@sync_to_async
def level3():
level4()

level3 = sync_to_async(level3, thread_sensitive=True)

# Async level 4
@async_to_sync
async def level4():
Expand All @@ -396,6 +390,29 @@ async def level4():
assert result["thread"] == threading.current_thread()


def test_thread_sensitive_disabled():
"""
Tests that we can disable thread sensitivity and make things run in
separate threads.
"""

result = {}

# Middle async function
@async_to_sync
async def middle():
await inner()

# Inner sync function
@sync_to_async(thread_sensitive=False)
def inner():
result["thread"] = threading.current_thread()

# Run it
middle()
assert result["thread"] != threading.current_thread()


class ASGITest(TestCase):
"""
Tests collection of async cases inside classes
Expand Down

0 comments on commit 7becc9d

Please sign in to comment.