Repository: sympy/sympy
Pull request: Make global parameters thread-local
<!-- Your title above should be a short description of what
was changed. Do not include the issue number in the title. -->

#### References to other Issues or PRs
<!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
format, e.g. "Fixes #1234". See
https://github.com/blog/1506-closing-issues-via-pull-requests . Please also
write a comment on that issue linking back to this pull request once it is
open. -->
Fixes #18161


#### Brief description of what is fixed or changed
Renamed `sympy/core/evaluate` to `sympy/core/parameters`.
Made the global parameters thread-local. Now, `global_parameters.evaluate` and `global_parameters.distribute` must be used instead of previous `global_evaluate[0]` and `global_distribute[0]`.

#### Other comments
Cache must be cleared at the beginning and end of each thread because cache is not thread-local.

#### Release Notes

<!-- Write the release notes for this release below. See
https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more information
on how to write release notes. The bot will check your release notes
automatically to see if they are formatted correctly. -->

<!-- BEGIN RELEASE NOTES -->
* core
    * Renamed `core/evaluate` to `core/parameters`
    * Added `global_parameters`, which is a thread-local storage for global parameters such as `evaluate`, `distribute`, etc.
<!-- END RELEASE NOTES -->


Problem statement:
Suggestion on `core.evaluate` module
As I understand, `core.evaluate` module is first developed to handle the global value of `evaluate` parameter. Then, it is extended to handle `distribute` parameter as well.
Since more global parameters might appear in the future, I think this module can be renamed to `core.parameters` for clarity.

Besides that, if more parameters are added, it will be annoying to have all `global_foo[0]`, `global_bar[0]`, and so on. I am thinking of a dict-like handler named `global_parameters` to manage every global parameters. It will behave like this:

1. Its `__getitem__()` method returns `global_foo` object.
```
>>> global_parameters
{'evaluate': [True], 'distribute': [True]}
>>> global_parameters['evaluate']
[True]
```

2. It has `foo` property that returns or sets the value of global `foo`.
```
>>> global_parameters.evaluate
True
>>> global_parameters.evaluate = False
>>> global_parameters.evaluate
False
>>> global_parameters
{'evaluate': [False], 'distribute': [True]}
```

3. Its properties are not `bool` - They are callable new classes so that they can be used as context manager.
```
>>> from sympy.abc import x
>>> with global_parameters.evaluate(False):
         print(x + x)
x + x
```

I have already written a code which satisfies suggestion 1 and 2. It seems to be working well. How does everyone think about it?

Hints from issue discussion:
Is your code thread-safe?
> Is your code thread-safe?

I didn't check it.
`global_parameters` is singleton and relegates every operations to `global_foo` it contains, so hopefully it will cause no problem as long as `global_foo` does the job right.

Can you suggest the way to check its thread safety?
We should really use thread local storage rather than global variables. Mutable global variables that are not thread-local are not thread-safe.
> We should really use thread local storage rather than global variables. Mutable global variables that are not thread-local are not thread-safe.

Thanks. I didn't know about thread safety before. I'm curious: does the current approach in `core.evaluate` satisfies thread safety? How does it achieve this?
Also, then how can I ensure the thread safety of `global_parameters`? Will blocking suggestion 2 (make the global parameter mutable by implementing setter) do? 
I don't think that the current approach is thread-safe because it just uses a list. That list will be shared between threads so if one thread sets global evaluate to True then it will affect the other threads. To be thread-safe this global needs to use thread-local storage.

Commit message: Make global parameters thread-local

Fixes issue #18161.
Changed the name of sympy/core/evaluate to sympy/core/parameters.
Made the global parameters thread-local. However, cache must be cleared 
at the beginning and end of each thread because cache is not 
thread-local.

Patch to review:
diff --git a/sympy/combinatorics/permutations.py b/sympy/combinatorics/permutations.py
index 79946ad7a3a3..b7fe2bf707d5 100644
--- a/sympy/combinatorics/permutations.py
+++ b/sympy/combinatorics/permutations.py
@@ -4,7 +4,7 @@
 from collections import defaultdict
 
 from sympy.core.basic import Atom, Basic
-from sympy.core.evaluate import global_evaluate
+from sympy.core.parameters import global_parameters
 from sympy.core.expr import Expr
 from sympy.core.compatibility import \
     is_sequence, reduce, range, as_int, Iterable
@@ -3002,7 +3002,7 @@ class AppliedPermutation(Expr):
     >>> _.subs(x, 1)
     2
     """
-    def __new__(cls, perm, x, evaluate=global_evaluate[0]):
+    def __new__(cls, perm, x, evaluate=global_parameters.evaluate):
         perm = _sympify(perm)
         x = _sympify(x)
 
diff --git a/sympy/core/__init__.py b/sympy/core/__init__.py
index 380a72ea80bc..f92a626aadb7 100644
--- a/sympy/core/__init__.py
+++ b/sympy/core/__init__.py
@@ -26,7 +26,7 @@
 from .evalf import PrecisionExhausted, N
 from .containers import Tuple, Dict
 from .exprtools import gcd_terms, factor_terms, factor_nc
-from .evaluate import evaluate
+from .parameters import evaluate
 
 # expose singletons
 Catalan = S.Catalan
diff --git a/sympy/core/add.py b/sympy/core/add.py
index f4ce7ceb0e25..b572c7b88b06 100644
--- a/sympy/core/add.py
+++ b/sympy/core/add.py
@@ -5,7 +5,7 @@
 
 from .basic import Basic
 from .compatibility import reduce, is_sequence, range
-from .evaluate import global_distribute
+from .parameters import global_parameters
 from .logic import _fuzzy_group, fuzzy_or, fuzzy_not
 from .singleton import S
 from .operations import AssocOp
@@ -1085,7 +1085,7 @@ def _mpc_(self):
         return (Float(re_part)._mpf_, Float(im_part)._mpf_)
 
     def __neg__(self):
-        if not global_distribute[0]:
+        if not global_parameters.distribute:
             return super(Add, self).__neg__()
         return Add(*[-i for i in self.args])
 
diff --git a/sympy/core/evaluate.py b/sympy/core/evaluate.py
deleted file mode 100644
index 0cd4296783e6..000000000000
--- a/sympy/core/evaluate.py
+++ /dev/null
@@ -1,72 +0,0 @@
-from .cache import clear_cache
-from contextlib import contextmanager
-
-
-class _global_function(list):
-    """ The cache must be cleared whenever _global_function is changed. """
-
-    def __setitem__(self, key, value):
-        if (self[key] != value):
-            clear_cache()
-        super(_global_function, self).__setitem__(key, value)
-
-
-global_evaluate = _global_function([True])
-global_distribute = _global_function([True])
-
-
-@contextmanager
-def evaluate(x):
-    """ Control automatic evaluation
-
-    This context manager controls whether or not all SymPy functions evaluate
-    by default.
-
-    Note that much of SymPy expects evaluated expressions.  This functionality
-    is experimental and is unlikely to function as intended on large
-    expressions.
-
-    Examples
-    ========
-
-    >>> from sympy.abc import x
-    >>> from sympy.core.evaluate import evaluate
-    >>> print(x + x)
-    2*x
-    >>> with evaluate(False):
-    ...     print(x + x)
-    x + x
-    """
-
-    old = global_evaluate[0]
-
-    global_evaluate[0] = x
-    yield
-    global_evaluate[0] = old
-
-
-@contextmanager
-def distribute(x):
-    """ Control automatic distribution of Number over Add
-
-    This context manager controls whether or not Mul distribute Number over
-    Add. Plan is to avoid distributing Number over Add in all of sympy. Once
-    that is done, this contextmanager will be removed.
-
-    Examples
-    ========
-
-    >>> from sympy.abc import x
-    >>> from sympy.core.evaluate import distribute
-    >>> print(2*(x + 1))
-    2*x + 2
-    >>> with distribute(False):
-    ...     print(2*(x + 1))
-    2*(x + 1)
-    """
-
-    old = global_distribute[0]
-
-    global_distribute[0] = x
-    yield
-    global_distribute[0] = old
diff --git a/sympy/core/function.py b/sympy/core/function.py
index 47e3a56ae8c2..8e88078803aa 100644
--- a/sympy/core/function.py
+++ b/sympy/core/function.py
@@ -46,7 +46,7 @@
 
 from sympy.core.compatibility import string_types, with_metaclass, PY3, range
 from sympy.core.containers import Tuple, Dict
-from sympy.core.evaluate import global_evaluate
+from sympy.core.parameters import global_parameters
 from sympy.core.logic import fuzzy_and
 from sympy.utilities import default_sort_key
 from sympy.utilities.exceptions import SymPyDeprecationWarning
@@ -276,7 +276,7 @@ def __new__(cls, *args, **options):
         from sympy.sets.sets import FiniteSet
 
         args = list(map(sympify, args))
-        evaluate = options.pop('evaluate', global_evaluate[0])
+        evaluate = options.pop('evaluate', global_parameters.evaluate)
         # WildFunction (and anything else like it) may have nargs defined
         # and we throw that value away here
         options.pop('nargs', None)
@@ -469,7 +469,7 @@ def __new__(cls, *args, **options):
                 'plural': 's'*(min(cls.nargs) != 1),
                 'given': n})
 
-        evaluate = options.get('evaluate', global_evaluate[0])
+        evaluate = options.get('evaluate', global_parameters.evaluate)
         result = super(Function, cls).__new__(cls, *args, **options)
         if evaluate and isinstance(result, cls) and result.args:
             pr2 = min(cls._should_evalf(a) for a in result.args)
diff --git a/sympy/core/mul.py b/sympy/core/mul.py
index 88501ce0e264..cbdcd4590fa0 100644
--- a/sympy/core/mul.py
+++ b/sympy/core/mul.py
@@ -12,7 +12,7 @@
 from .logic import fuzzy_not, _fuzzy_group
 from .compatibility import reduce, range
 from .expr import Expr
-from .evaluate import global_distribute
+from .parameters import global_parameters
 
 
 
@@ -202,7 +202,7 @@ def flatten(cls, seq):
                     if r is not S.One:  # 2-arg hack
                         # leave the Mul as a Mul
                         rv = [cls(a*r, b, evaluate=False)], [], None
-                    elif global_distribute[0] and b.is_commutative:
+                    elif global_parameters.distribute and b.is_commutative:
                         r, b = b.as_coeff_Add()
                         bargs = [_keep_coeff(a, bi) for bi in Add.make_args(b)]
                         _addsort(bargs)
@@ -626,7 +626,7 @@ def _handle_for_oo(c_part, coeff_sign):
             c_part.insert(0, coeff)
 
         # we are done
-        if (global_distribute[0] and not nc_part and len(c_part) == 2 and
+        if (global_parameters.distribute and not nc_part and len(c_part) == 2 and
                 c_part[0].is_Number and c_part[0].is_finite and c_part[1].is_Add):
             # 2*(1+a) -> 2 + 2 * a
             coeff = c_part[0]
diff --git a/sympy/core/numbers.py b/sympy/core/numbers.py
index bdf38bfac63c..ea4117522919 100644
--- a/sympy/core/numbers.py
+++ b/sympy/core/numbers.py
@@ -30,7 +30,7 @@
     fnan as _mpf_nan, fzero, _normalize as mpf_normalize,
     prec_to_dps, fone, fnone)
 from sympy.utilities.misc import debug, filldedent
-from .evaluate import global_evaluate
+from .parameters import global_parameters
 
 from sympy.utilities.exceptions import SymPyDeprecationWarning
 
@@ -713,7 +713,7 @@ def sort_key(self, order=None):
 
     @_sympifyit('other', NotImplemented)
     def __add__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             if other is S.NaN:
                 return S.NaN
             elif other is S.Infinity:
@@ -724,7 +724,7 @@ def __add__(self, other):
 
     @_sympifyit('other', NotImplemented)
     def __sub__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             if other is S.NaN:
                 return S.NaN
             elif other is S.Infinity:
@@ -735,7 +735,7 @@ def __sub__(self, other):
 
     @_sympifyit('other', NotImplemented)
     def __mul__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             if other is S.NaN:
                 return S.NaN
             elif other is S.Infinity:
@@ -758,7 +758,7 @@ def __mul__(self, other):
 
     @_sympifyit('other', NotImplemented)
     def __div__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             if other is S.NaN:
                 return S.NaN
             elif other is S.Infinity or other is S.NegativeInfinity:
@@ -1293,28 +1293,28 @@ def __neg__(self):
 
     @_sympifyit('other', NotImplemented)
     def __add__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             rhs, prec = other._as_mpf_op(self._prec)
             return Float._new(mlib.mpf_add(self._mpf_, rhs, prec, rnd), prec)
         return Number.__add__(self, other)
 
     @_sympifyit('other', NotImplemented)
     def __sub__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             rhs, prec = other._as_mpf_op(self._prec)
             return Float._new(mlib.mpf_sub(self._mpf_, rhs, prec, rnd), prec)
         return Number.__sub__(self, other)
 
     @_sympifyit('other', NotImplemented)
     def __mul__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             rhs, prec = other._as_mpf_op(self._prec)
             return Float._new(mlib.mpf_mul(self._mpf_, rhs, prec, rnd), prec)
         return Number.__mul__(self, other)
 
     @_sympifyit('other', NotImplemented)
     def __div__(self, other):
-        if isinstance(other, Number) and other != 0 and global_evaluate[0]:
+        if isinstance(other, Number) and other != 0 and global_parameters.evaluate:
             rhs, prec = other._as_mpf_op(self._prec)
             return Float._new(mlib.mpf_div(self._mpf_, rhs, prec, rnd), prec)
         return Number.__div__(self, other)
@@ -1323,24 +1323,24 @@ def __div__(self, other):
 
     @_sympifyit('other', NotImplemented)
     def __mod__(self, other):
-        if isinstance(other, Rational) and other.q != 1 and global_evaluate[0]:
+        if isinstance(other, Rational) and other.q != 1 and global_parameters.evaluate:
             # calculate mod with Rationals, *then* round the result
             return Float(Rational.__mod__(Rational(self), other),
                          precision=self._prec)
-        if isinstance(other, Float) and global_evaluate[0]:
+        if isinstance(other, Float) and global_parameters.evaluate:
             r = self/other
             if r == int(r):
                 return Float(0, precision=max(self._prec, other._prec))
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             rhs, prec = other._as_mpf_op(self._prec)
             return Float._new(mlib.mpf_mod(self._mpf_, rhs, prec, rnd), prec)
         return Number.__mod__(self, other)
 
     @_sympifyit('other', NotImplemented)
     def __rmod__(self, other):
-        if isinstance(other, Float) and global_evaluate[0]:
+        if isinstance(other, Float) and global_parameters.evaluate:
             return other.__mod__(self)
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             rhs, prec = other._as_mpf_op(self._prec)
             return Float._new(mlib.mpf_mod(rhs, self._mpf_, prec, rnd), prec)
         return Number.__rmod__(self, other)
@@ -1698,7 +1698,7 @@ def __neg__(self):
 
     @_sympifyit('other', NotImplemented)
     def __add__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, Integer):
                 return Rational(self.p + self.q*other.p, self.q, 1)
             elif isinstance(other, Rational):
@@ -1713,7 +1713,7 @@ def __add__(self, other):
 
     @_sympifyit('other', NotImplemented)
     def __sub__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, Integer):
                 return Rational(self.p - self.q*other.p, self.q, 1)
             elif isinstance(other, Rational):
@@ -1725,7 +1725,7 @@ def __sub__(self, other):
         return Number.__sub__(self, other)
     @_sympifyit('other', NotImplemented)
     def __rsub__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, Integer):
                 return Rational(self.q*other.p - self.p, self.q, 1)
             elif isinstance(other, Rational):
@@ -1737,7 +1737,7 @@ def __rsub__(self, other):
         return Number.__rsub__(self, other)
     @_sympifyit('other', NotImplemented)
     def __mul__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, Integer):
                 return Rational(self.p*other.p, self.q, igcd(other.p, self.q))
             elif isinstance(other, Rational):
@@ -1751,7 +1751,7 @@ def __mul__(self, other):
 
     @_sympifyit('other', NotImplemented)
     def __div__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, Integer):
                 if self.p and other.p == S.Zero:
                     return S.ComplexInfinity
@@ -1766,7 +1766,7 @@ def __div__(self, other):
         return Number.__div__(self, other)
     @_sympifyit('other', NotImplemented)
     def __rdiv__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, Integer):
                 return Rational(other.p*self.q, self.p, igcd(self.p, other.p))
             elif isinstance(other, Rational):
@@ -1780,7 +1780,7 @@ def __rdiv__(self, other):
 
     @_sympifyit('other', NotImplemented)
     def __mod__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, Rational):
                 n = (self.p*other.q) // (other.p*self.q)
                 return Rational(self.p*other.q - n*other.p*self.q, self.q*other.q)
@@ -2141,14 +2141,14 @@ def __abs__(self):
 
     def __divmod__(self, other):
         from .containers import Tuple
-        if isinstance(other, Integer) and global_evaluate[0]:
+        if isinstance(other, Integer) and global_parameters.evaluate:
             return Tuple(*(divmod(self.p, other.p)))
         else:
             return Number.__divmod__(self, other)
 
     def __rdivmod__(self, other):
         from .containers import Tuple
-        if isinstance(other, integer_types) and global_evaluate[0]:
+        if isinstance(other, integer_types) and global_parameters.evaluate:
             return Tuple(*(divmod(other, self.p)))
         else:
             try:
@@ -2162,7 +2162,7 @@ def __rdivmod__(self, other):
 
     # TODO make it decorator + bytecodehacks?
     def __add__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, integer_types):
                 return Integer(self.p + other)
             elif isinstance(other, Integer):
@@ -2174,7 +2174,7 @@ def __add__(self, other):
             return Add(self, other)
 
     def __radd__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, integer_types):
                 return Integer(other + self.p)
             elif isinstance(other, Rational):
@@ -2183,7 +2183,7 @@ def __radd__(self, other):
         return Rational.__radd__(self, other)
 
     def __sub__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, integer_types):
                 return Integer(self.p - other)
             elif isinstance(other, Integer):
@@ -2194,7 +2194,7 @@ def __sub__(self, other):
         return Rational.__sub__(self, other)
 
     def __rsub__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, integer_types):
                 return Integer(other - self.p)
             elif isinstance(other, Rational):
@@ -2203,7 +2203,7 @@ def __rsub__(self, other):
         return Rational.__rsub__(self, other)
 
     def __mul__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, integer_types):
                 return Integer(self.p*other)
             elif isinstance(other, Integer):
@@ -2214,7 +2214,7 @@ def __mul__(self, other):
         return Rational.__mul__(self, other)
 
     def __rmul__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, integer_types):
                 return Integer(other*self.p)
             elif isinstance(other, Rational):
@@ -2223,7 +2223,7 @@ def __rmul__(self, other):
         return Rational.__rmul__(self, other)
 
     def __mod__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, integer_types):
                 return Integer(self.p % other)
             elif isinstance(other, Integer):
@@ -2232,7 +2232,7 @@ def __mod__(self, other):
         return Rational.__mod__(self, other)
 
     def __rmod__(self, other):
-        if global_evaluate[0]:
+        if global_parameters.evaluate:
             if isinstance(other, integer_types):
                 return Integer(other % self.p)
             elif isinstance(other, Integer):
@@ -2844,7 +2844,7 @@ def evalf(self, prec=None, **options):
 
     @_sympifyit('other', NotImplemented)
     def __add__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             if other is S.NegativeInfinity or other is S.NaN:
                 return S.NaN
             return self
@@ -2853,7 +2853,7 @@ def __add__(self, other):
 
     @_sympifyit('other', NotImplemented)
     def __sub__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             if other is S.Infinity or other is S.NaN:
                 return S.NaN
             return self
@@ -2865,7 +2865,7 @@ def __rsub__(self, other):
 
     @_sympifyit('other', NotImplemented)
     def __mul__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             if other.is_zero or other is S.NaN:
                 return S.NaN
             if other.is_extended_positive:
@@ -2876,7 +2876,7 @@ def __mul__(self, other):
 
     @_sympifyit('other', NotImplemented)
     def __div__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             if other is S.Infinity or \
                 other is S.NegativeInfinity or \
                     other is S.NaN:
@@ -3009,7 +3009,7 @@ def evalf(self, prec=None, **options):
 
     @_sympifyit('other', NotImplemented)
     def __add__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             if other is S.Infinity or other is S.NaN:
                 return S.NaN
             return self
@@ -3018,7 +3018,7 @@ def __add__(self, other):
 
     @_sympifyit('other', NotImplemented)
     def __sub__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             if other is S.NegativeInfinity or other is S.NaN:
                 return S.NaN
             return self
@@ -3030,7 +3030,7 @@ def __rsub__(self, other):
 
     @_sympifyit('other', NotImplemented)
     def __mul__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             if other.is_zero or other is S.NaN:
                 return S.NaN
             if other.is_extended_positive:
@@ -3041,7 +3041,7 @@ def __mul__(self, other):
 
     @_sympifyit('other', NotImplemented)
     def __div__(self, other):
-        if isinstance(other, Number) and global_evaluate[0]:
+        if isinstance(other, Number) and global_parameters.evaluate:
             if other is S.Infinity or \
                 other is S.NegativeInfinity or \
                     other is S.NaN:
diff --git a/sympy/core/operations.py b/sympy/core/operations.py
index dffcf2dfa674..b16a1d4c899f 100644
--- a/sympy/core/operations.py
+++ b/sympy/core/operations.py
@@ -5,7 +5,7 @@
 from sympy.core.cache import cacheit
 from sympy.core.compatibility import ordered, range
 from sympy.core.logic import fuzzy_and
-from sympy.core.evaluate import global_evaluate
+from sympy.core.parameters import global_parameters
 from sympy.utilities.iterables import sift
 
 
@@ -33,7 +33,7 @@ def __new__(cls, *args, **options):
 
         evaluate = options.get('evaluate')
         if evaluate is None:
-            evaluate = global_evaluate[0]
+            evaluate = global_parameters.evaluate
         if not evaluate:
             obj = cls._from_args(args)
             obj = cls._exec_constructor_postprocessors(obj)
diff --git a/sympy/core/parameters.py b/sympy/core/parameters.py
new file mode 100644
index 000000000000..b91b762d8197
--- /dev/null
+++ b/sympy/core/parameters.py
@@ -0,0 +1,120 @@
+"""Thread-safe global parameters"""
+
+from .cache import clear_cache
+from contextlib import contextmanager
+from threading import local
+
+class _global_parameters(local):
+    """
+    Thread-local global parameters.
+
+    Explanation
+    ===========
+
+    This class generates thread-local container for SymPy's global parameters.
+    Every global parameters must be passed as keyword argument when generating
+    its instance.
+    A variable, `global_parameters` is provided as default instance for this class.
+
+    Examples
+    ========
+
+    >>> from sympy.abc import x
+    >>> from sympy.core.cache import clear_cache
+    >>> from sympy.core.parameters import global_parameters as gp
+
+    >>> gp.evaluate
+    True
+    >>> x+x
+    2*x
+
+    Since SymPy's cache is not thread-local, it must be cleared when the thread
+    begins and ends.
+
+    >>> def f():
+    ...     clear_cache()
+    ...     gp.evaluate = False
+    ...     print(x+x)
+    ...     clear_cache()
+    >>> import threading
+    >>> thread = threading.Thread(target=f)
+    >>> thread.start()
+    x + x
+    >>> thread.join()
+
+    >>> gp.evaluate
+    True
+    >>> x+x
+    2*x
+
+    References
+    ==========
+
+    .. [1] https://docs.python.org/3/library/threading.html
+
+    """
+    def __init__(self, **kwargs):
+        self.__dict__.update(kwargs)
+
+    def __setattr__(self, name, value):
+        if getattr(self, name) != value:
+            clear_cache()
+        return super(_global_parameters, self).__setattr__(name, value)
+
+global_parameters = _global_parameters(evaluate=True, distribute=True)
+
+@contextmanager
+def evaluate(x):
+    """ Control automatic evaluation
+
+    This context manager controls whether or not all SymPy functions evaluate
+    by default.
+
+    Note that much of SymPy expects evaluated expressions.  This functionality
+    is experimental and is unlikely to function as intended on large
+    expressions.
+
+    Examples
+    ========
+
+    >>> from sympy.abc import x
+    >>> from sympy.core.parameters import evaluate
+    >>> print(x + x)
+    2*x
+    >>> with evaluate(False):
+    ...     print(x + x)
+    x + x
+    """
+
+    old = global_parameters.evaluate
+
+    global_parameters.evaluate = x
+    yield
+    global_parameters.evaluate = old
+
+
+@contextmanager
+def distribute(x):
+    """ Control automatic distribution of Number over Add
+
+    This context manager controls whether or not Mul distribute Number over
+    Add. Plan is to avoid distributing Number over Add in all of sympy. Once
+    that is done, this contextmanager will be removed.
+
+    Examples
+    ========
+
+    >>> from sympy.abc import x
+    >>> from sympy.core.parameters import distribute
+    >>> print(2*(x + 1))
+    2*x + 2
+    >>> with distribute(False):
+    ...     print(2*(x + 1))
+    2*(x + 1)
+    """
+
+    old = global_parameters.distribute
+
+    global_parameters.distribute = x
+    yield
+    global_parameters.distribute = old
diff --git a/sympy/core/power.py b/sympy/core/power.py
index 89e9d9648734..06859df54567 100644
--- a/sympy/core/power.py
+++ b/sympy/core/power.py
@@ -11,7 +11,7 @@
     expand_mul)
 from .logic import fuzzy_bool, fuzzy_not, fuzzy_and
 from .compatibility import as_int, range
-from .evaluate import global_evaluate
+from .parameters import global_parameters
 from sympy.utilities.iterables import sift
 
 from mpmath.libmp import sqrtrem as mpmath_sqrtrem
@@ -257,7 +257,7 @@ class Pow(Expr):
     @cacheit
     def __new__(cls, b, e, evaluate=None):
         if evaluate is None:
-            evaluate = global_evaluate[0]
+            evaluate = global_parameters.evaluate
         from sympy.functions.elementary.exponential import exp_polar
 
         b = _sympify(b)
diff --git a/sympy/core/relational.py b/sympy/core/relational.py
index f4ede7de61ae..e77e02a2a477 100644
--- a/sympy/core/relational.py
+++ b/sympy/core/relational.py
@@ -7,7 +7,7 @@
 from .expr import Expr
 from .evalf import EvalfMixin
 from .sympify import _sympify
-from .evaluate import global_evaluate
+from .parameters import global_parameters
 
 from sympy.logic.boolalg import Boolean, BooleanAtom
 
@@ -474,7 +474,7 @@ def __new__(cls, lhs, rhs=None, **options):
         lhs = _sympify(lhs)
         rhs = _sympify(rhs)
 
-        evaluate = options.pop('evaluate', global_evaluate[0])
+        evaluate = options.pop('evaluate', global_parameters.evaluate)
 
         if evaluate:
             # If one expression has an _eval_Eq, return its results.
@@ -686,7 +686,7 @@ def __new__(cls, lhs, rhs, **options):
         lhs = _sympify(lhs)
         rhs = _sympify(rhs)
 
-        evaluate = options.pop('evaluate', global_evaluate[0])
+        evaluate = options.pop('evaluate', global_parameters.evaluate)
 
         if evaluate:
             is_equal = Equality(lhs, rhs)
@@ -733,7 +733,7 @@ def __new__(cls, lhs, rhs, **options):
         lhs = _sympify(lhs)
         rhs = _sympify(rhs)
 
-        evaluate = options.pop('evaluate', global_evaluate[0])
+        evaluate = options.pop('evaluate', global_parameters.evaluate)
 
         if evaluate:
             # First we invoke the appropriate inequality method of `lhs`
diff --git a/sympy/core/sympify.py b/sympy/core/sympify.py
index ed4ea6ce5eab..0ea6099bca89 100644
--- a/sympy/core/sympify.py
+++ b/sympy/core/sympify.py
@@ -6,7 +6,7 @@
 
 from .core import all_classes as sympy_classes
 from .compatibility import iterable, string_types, range
-from .evaluate import global_evaluate
+from .parameters import global_parameters
 
 
 class SympifyError(ValueError):
@@ -288,10 +288,7 @@ def sympify(a, locals=None, convert_xor=True, strict=False, rational=False,
             return a
 
     if evaluate is None:
-        if global_evaluate[0] is False:
-            evaluate = global_evaluate[0]
-        else:
-            evaluate = True
+        evaluate = global_parameters.evaluate
 
     # Support for basic numpy datatypes
     # Note that this check exists to avoid importing NumPy when not necessary
diff --git a/sympy/core/tests/test_arit.py b/sympy/core/tests/test_arit.py
index 738806ef40ce..acce9d7d217e 100644
--- a/sympy/core/tests/test_arit.py
+++ b/sympy/core/tests/test_arit.py
@@ -3,7 +3,7 @@
         oo, zoo, Integer, sign, im, nan, Dummy, factorial, comp, floor
 )
 from sympy.core.compatibility import long, range
-from sympy.core.evaluate import distribute
+from sympy.core.parameters import distribute
 from sympy.core.expr import unchanged
 from sympy.utilities.iterables import cartes
 from sympy.utilities.pytest import XFAIL, raises
diff --git a/sympy/core/tests/test_evaluate.py b/sympy/core/tests/test_parameters.py
similarity index 98%
rename from sympy/core/tests/test_evaluate.py
rename to sympy/core/tests/test_parameters.py
index e2b1db34410a..1f60b801ca34 100644
--- a/sympy/core/tests/test_evaluate.py
+++ b/sympy/core/tests/test_parameters.py
@@ -1,5 +1,5 @@
 from sympy.abc import x, y
-from sympy.core.evaluate import evaluate
+from sympy.core.parameters import evaluate
 from sympy.core import Mul, Add, Pow, S
 from sympy import sqrt, oo, pretty, nan
 
diff --git a/sympy/geometry/ellipse.py b/sympy/geometry/ellipse.py
index b84c4e2bdf2f..3084a84893e6 100644
--- a/sympy/geometry/ellipse.py
+++ b/sympy/geometry/ellipse.py
@@ -10,7 +10,7 @@
 
 from sympy import Expr, Eq
 from sympy.core import S, pi, sympify
-from sympy.core.evaluate import global_evaluate
+from sympy.core.parameters import global_parameters
 from sympy.core.logic import fuzzy_bool
 from sympy.core.numbers import Rational, oo
 from sympy.core.compatibility import ordered
@@ -1546,7 +1546,7 @@ class Circle(Ellipse):
     def __new__(cls, *args, **kwargs):
         from sympy.geometry.util import find
         from .polygon import Triangle
-        evaluate = kwargs.get('evaluate', global_evaluate[0])
+        evaluate = kwargs.get('evaluate', global_parameters.evaluate)
         if len(args) == 1 and isinstance(args[0], Expr):
             x = kwargs.get('x', 'x')
             y = kwargs.get('y', 'y')
diff --git a/sympy/geometry/point.py b/sympy/geometry/point.py
index 219b8bfdca3b..7d8e40e617ad 100644
--- a/sympy/geometry/point.py
+++ b/sympy/geometry/point.py
@@ -30,7 +30,7 @@
 from sympy.functions.elementary.complexes import im
 from sympy.matrices import Matrix
 from sympy.core.numbers import Float
-from sympy.core.evaluate import global_evaluate
+from sympy.core.parameters import global_parameters
 from sympy.core.add import Add
 from sympy.utilities.iterables import uniq
 from sympy.utilities.misc import filldedent, func_name, Undecidable
@@ -106,7 +106,7 @@ class Point(GeometryEntity):
     is_Point = True
 
     def __new__(cls, *args, **kwargs):
-        evaluate = kwargs.get('evaluate', global_evaluate[0])
+        evaluate = kwargs.get('evaluate', global_parameters.evaluate)
         on_morph = kwargs.get('on_morph', 'ignore')
 
         # unpack into coords
diff --git a/sympy/series/sequences.py b/sympy/series/sequences.py
index 708bb19d8543..91e438b5ef6f 100644
--- a/sympy/series/sequences.py
+++ b/sympy/series/sequences.py
@@ -6,7 +6,7 @@
                                       is_sequence, iterable, ordered)
 from sympy.core.containers import Tuple
 from sympy.core.decorators import call_highest_priority
-from sympy.core.evaluate import global_evaluate
+from sympy.core.parameters import global_parameters
 from sympy.core.function import UndefinedFunction
 from sympy.core.mul import Mul
 from sympy.core.numbers import Integer
@@ -1005,7 +1005,7 @@ class SeqAdd(SeqExprOp):
     """
 
     def __new__(cls, *args, **kwargs):
-        evaluate = kwargs.get('evaluate', global_evaluate[0])
+        evaluate = kwargs.get('evaluate', global_parameters.evaluate)
 
         # flatten inputs
         args = list(args)
@@ -1114,7 +1114,7 @@ class SeqMul(SeqExprOp):
     """
 
     def __new__(cls, *args, **kwargs):
-        evaluate = kwargs.get('evaluate', global_evaluate[0])
+        evaluate = kwargs.get('evaluate', global_parameters.evaluate)
 
         # flatten inputs
         args = list(args)
diff --git a/sympy/sets/powerset.py b/sympy/sets/powerset.py
index 11cd5c8cc679..c34ebd7dc5c7 100644
--- a/sympy/sets/powerset.py
+++ b/sympy/sets/powerset.py
@@ -1,7 +1,7 @@
 from __future__ import print_function, division
 
 from sympy.core.decorators import _sympifyit
-from sympy.core.evaluate import global_evaluate
+from sympy.core.parameters import global_parameters
 from sympy.core.logic import fuzzy_bool
 from sympy.core.singleton import S
 from sympy.core.sympify import _sympify
@@ -71,7 +71,10 @@ class PowerSet(Set):
 
     .. [2] https://en.wikipedia.org/wiki/Axiom_of_power_set
     """
-    def __new__(cls, arg, evaluate=global_evaluate[0]):
+    def __new__(cls, arg, evaluate=None):
+        if evaluate is None:
+            evaluate=global_parameters.evaluate
+
         arg = _sympify(arg)
 
         if not isinstance(arg, Set):
diff --git a/sympy/sets/sets.py b/sympy/sets/sets.py
index 96b6751597a0..247cb06c65a0 100644
--- a/sympy/sets/sets.py
+++ b/sympy/sets/sets.py
@@ -10,7 +10,7 @@
 from sympy.core.containers import Tuple
 from sympy.core.decorators import deprecated
 from sympy.core.evalf import EvalfMixin
-from sympy.core.evaluate import global_evaluate
+from sympy.core.parameters import global_parameters
 from sympy.core.expr import Expr
 from sympy.core.logic import fuzzy_bool, fuzzy_or, fuzzy_and, fuzzy_not
 from sympy.core.numbers import Float
@@ -1157,7 +1157,7 @@ def zero(self):
         return S.UniversalSet
 
     def __new__(cls, *args, **kwargs):
-        evaluate = kwargs.get('evaluate', global_evaluate[0])
+        evaluate = kwargs.get('evaluate', global_parameters.evaluate)
 
         # flatten inputs to merge intersections and iterables
         args = _sympify(args)
@@ -1330,7 +1330,7 @@ def zero(self):
         return S.EmptySet
 
     def __new__(cls, *args, **kwargs):
-        evaluate = kwargs.get('evaluate', global_evaluate[0])
+        evaluate = kwargs.get('evaluate', global_parameters.evaluate)
 
         # flatten inputs to merge intersections and iterables
         args = list(ordered(set(_sympify(args))))
@@ -1752,7 +1752,7 @@ class FiniteSet(Set, EvalfMixin):
     is_finite_set = True
 
     def __new__(cls, *args, **kwargs):
-        evaluate = kwargs.get('evaluate', global_evaluate[0])
+        evaluate = kwargs.get('evaluate', global_parameters.evaluate)
         if evaluate:
             args = list(map(sympify, args))
 
diff --git a/sympy/simplify/radsimp.py b/sympy/simplify/radsimp.py
index 32e906e85335..e074cc621f4c 100644
--- a/sympy/simplify/radsimp.py
+++ b/sympy/simplify/radsimp.py
@@ -7,7 +7,7 @@
 from sympy.core import expand_power_base, sympify, Add, S, Mul, Derivative, Pow, symbols, expand_mul
 from sympy.core.add import _unevaluated_Add
 from sympy.core.compatibility import iterable, ordered, default_sort_key
-from sympy.core.evaluate import global_evaluate
+from sympy.core.parameters import global_parameters
 from sympy.core.exprtools import Factors, gcd_terms
 from sympy.core.function import _mexpand
 from sympy.core.mul import _keep_coeff, _unevaluated_Mul
@@ -163,7 +163,7 @@ def collect(expr, syms, func=None, evaluate=None, exact=False, distribute_order_
     syms = list(syms) if iterable(syms) else [syms]
 
     if evaluate is None:
-        evaluate = global_evaluate[0]
+        evaluate = global_parameters.evaluate
 
     def make_expression(terms):
         product = []
@@ -496,7 +496,7 @@ def collect_sqrt(expr, evaluate=None):
     collect, collect_const, rcollect
     """
     if evaluate is None:
-        evaluate = global_evaluate[0]
+        evaluate = global_parameters.evaluate
     # this step will help to standardize any complex arguments
     # of sqrts
     coeff, expr = expr.as_content_primitive()
diff --git a/sympy/simplify/simplify.py b/sympy/simplify/simplify.py
index ac551a89db08..eca7c21256bc 100644
--- a/sympy/simplify/simplify.py
+++ b/sympy/simplify/simplify.py
@@ -6,7 +6,7 @@
                         expand_func, Function, Dummy, Expr, factor_terms,
                         expand_power_exp, Eq)
 from sympy.core.compatibility import iterable, ordered, range, as_int
-from sympy.core.evaluate import global_evaluate
+from sympy.core.parameters import global_parameters
 from sympy.core.function import expand_log, count_ops, _mexpand, _coeff_isneg, \
     nfloat, expand_mul, expand_multinomial
 from sympy.core.numbers import Float, I, pi, Rational, Integer
@@ -377,7 +377,7 @@ def signsimp(expr, evaluate=None):
 
     """
     if evaluate is None:
-        evaluate = global_evaluate[0]
+        evaluate = global_parameters.evaluate
     expr = sympify(expr)
     if not isinstance(expr, Expr) or expr.is_Atom:
         return expr
diff --git a/sympy/stats/symbolic_probability.py b/sympy/stats/symbolic_probability.py
index 7c56f9a47abc..0f114bc1c39c 100644
--- a/sympy/stats/symbolic_probability.py
+++ b/sympy/stats/symbolic_probability.py
@@ -2,7 +2,7 @@
 
 from sympy import Expr, Add, Mul, S, Integral, Eq, Sum, Symbol
 from sympy.core.compatibility import default_sort_key
-from sympy.core.evaluate import global_evaluate
+from sympy.core.parameters import global_parameters
 from sympy.core.sympify import _sympify
 from sympy.stats import variance, covariance
 from sympy.stats.rv import RandomSymbol, probability, expectation
@@ -324,7 +324,7 @@ def __new__(cls, arg1, arg2, condition=None, **kwargs):
         arg1 = _sympify(arg1)
         arg2 = _sympify(arg2)
 
-        if kwargs.pop('evaluate', global_evaluate[0]):
+        if kwargs.pop('evaluate', global_parameters.evaluate):
             arg1, arg2 = sorted([arg1, arg2], key=default_sort_key)
 
         if condition is None:
diff --git a/sympy/tensor/functions.py b/sympy/tensor/functions.py
index 26f129bf2d29..1fbf5e0439b4 100644
--- a/sympy/tensor/functions.py
+++ b/sympy/tensor/functions.py
@@ -1,6 +1,6 @@
 from sympy import Expr, S, Mul, sympify
 from sympy.core.compatibility import Iterable
-from sympy.core.evaluate import global_evaluate
+from sympy.core.parameters import global_parameters
 
 
 class TensorProduct(Expr):
@@ -15,7 +15,7 @@ def __new__(cls, *args, **kwargs):
         from sympy.strategies import flatten
 
         args = [sympify(arg) for arg in args]
-        evaluate = kwargs.get("evaluate", global_evaluate[0])
+        evaluate = kwargs.get("evaluate", global_parameters.evaluate)
 
         if not evaluate:
             obj = Expr.__new__(cls, *args)


Code review comments:
diff_hunk: @@ -0,0 +1,120 @@
+"""Thread-safe global parameters"""
+
+from .cache import clear_cache
+from contextlib import contextmanager
+from threading import local
+
+class _global_parameters(local):
+    """
+    Thread-local global parameters.
+
+    Explanation
+    ===========
+
+    This class generates thread-local container for SymPy's global parameters.
+    Every global parameters must be passed as keyword argument when generating
+    its instance.
+    A variable, `global_parameters` is provided as default instance for this class.
+
+    Examples
+    ========
+
+    >>> from sympy.abc import x
+    >>> from sympy.core.cache import clear_cache
+    >>> from sympy.core.parameters import global_parameters as gp
+
+    >>> gp.evaluate
+    True
+    >>> x+x
+    2*x
+
+    Since SymPy's cache is not thread-local, it must be cleared when the thread
+    begins and ends.
+
+    >>> def f():
+    ...     clear_cache()
+    ...     gp.evaluate = False
+    ...     print(x+x)
+    ...     clear_cache()
+    >>> import threading
+    >>> thread = threading.Thread(target=f)
+    >>> thread.start()
+    x + x
+    >>> thread.join()
+
+    >>> gp.evaluate
+    True
+    >>> x+x
+    2*x
+
+    References
+    ==========
+
+    .. [1] https://docs.python.org/3/library/threading.html
+
+    """
+    def __init__(self, **kwargs):
+        self.__dict__.update(kwargs)
+
+    def __setattr__(self, name, value):
+        if getattr(self, name) != value:
+            clear_cache()
+        return super(_global_parameters, self).__setattr__(name, value)
+
+global_parameters = _global_parameters(evaluate=True, distribute=True)
+
+@contextmanager
+def evaluate(x):
+    """ Control automatic evaluation
+
+    This context manager controls whether or not all SymPy functions evaluate
+    by default.
+
+    Note that much of SymPy expects evaluated expressions.  This functionality
+    is experimental and is unlikely to function as intended on large
+    expressions.
+
+    Examples
+    ========
+
+    >>> from sympy.abc import x
+    >>> from sympy.core.parameters import evaluate
+    >>> print(x + x)
+    2*x
+    >>> with evaluate(False):
+    ...     print(x + x)
+    x + x
+    """
+
+    old = global_parameters.evaluate
+
+    global_parameters.evaluate = x
+    yield
+    global_parameters.evaluate = old
path: sympy/core/parameters.py
text: @user1:
This should be done with `try/finally`:
```python
try:
    global_parameters.evaluate = x
    yield
finally:
    global_parameters.evaluate = old
```

diff_hunk: @@ -3002,7 +3002,7 @@ class AppliedPermutation(Expr):
     >>> _.subs(x, 1)
     2
     """
-    def __new__(cls, perm, x, evaluate=global_evaluate[0]):
+    def __new__(cls, perm, x, evaluate=global_parameters.evaluate):
path: sympy/combinatorics/permutations.py
text: @user1:
This was incorrect and should be fixed. Default values for function parameters are evaluated at the time of function definition not function call. I guess this isn't tested.