It’s not a hidden knowledge that many assignments are not atomic and we can face the word tearing. They are mostly related to CPU word length, synchronization, concurrency, etc.
However, things can be much worse when we’re dealing with interpreted languages or languages with no strict schema. In these languages, a “regular” assignment can also be non-atomic.
Let’s take Python. In Python, every object can be considered a dictionary of fields. This means that a single assignment may result in expanding the forementioned dictionary which may cause issues for some other thread. Let’s see an example:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import jsonpickle from concurrent.futures import ThreadPoolExecutor threads = 40 iterations = 1000 promises = [] class Sample: def __init__(self): self.big_property = [x for x in range(100000)] def serializer(s): jsonpickle.dumps(s) def result_setter(s): s.abc = "abc" with ThreadPoolExecutor(max_workers=threads) as executor: for x in range(iterations): s = Sample() promises.append(executor.submit(result_setter, s)) promises.append(executor.submit(serializer, s)) for promise in promises: promise.result() |
We have a Sample class that has one field initially, namely big_property.
We have two different types of tasks: the first one serializer uses the jsonpickle library to serialize an object to a JSON string. The second task result_setter sets a field on the object. We then run sufficiently many tasks to observe the issue.
If we’re unlucky enough, we’ll hit the following race condition: the first task starts serializing the object, then the first task is paused and the second task kicks in. The second tasks sets a field on the object. Normally, we could think this assignment is “atomic” as we only set a reference into a field. However, since the Python object is a dictionary of fields, we need to add new entry to the dictionary. Once the first task is resumed, it throws the following error:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
Traceback (most recent call last): File "<stdin>", line 2, in <module> File "/usr/lib/python3.11/concurrent/futures/_base.py", line 449, in result return self.__get_result() ^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.11/concurrent/futures/_base.py", line 401, in __get_result raise self._exception File "/usr/lib/python3.11/concurrent/futures/thread.py", line 58, in run result = self.fn(*self.args, **self.kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<stdin>", line 2, in serializer File "/.venv/lib/python3.11/site-packages/jsonpickle/pickler.py", line 166, in encode context.flatten(value, reset=reset), indent=indent, separators=separators ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/.venv/lib/python3.11/site-packages/jsonpickle/pickler.py", line 366, in flatten return self._flatten(obj) ^^^^^^^^^^^^^^^^^^ File "/.venv/lib/python3.11/site-packages/jsonpickle/pickler.py", line 326, in _flatten result = self._flatten_impl(obj) ^^^^^^^^^^^^^^^^^^^^^^^ File "/.venv/lib/python3.11/site-packages/jsonpickle/pickler.py", line 386, in _flatten_impl return self._pop(self._flatten_obj(obj)) ^^^^^^^^^^^^^^^^^^^^^^ File "/.venv/lib/python3.11/site-packages/jsonpickle/pickler.py", line 419, in _flatten_obj raise e File "/.venv/lib/python3.11/site-packages/jsonpickle/pickler.py", line 413, in _flatten_obj return flatten_func(obj) ^^^^^^^^^^^^^^^^^ File "/.venv/lib/python3.11/site-packages/jsonpickle/pickler.py", line 716, in _ref_obj_instance return self._flatten_obj_instance(obj) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/.venv/lib/python3.11/site-packages/jsonpickle/pickler.py", line 697, in _flatten_obj_instance return self._flatten_dict_obj(obj.__dict__, data, exclude=exclude) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/.venv/lib/python3.11/site-packages/jsonpickle/pickler.py", line 794, in _flatten_dict_obj for k, v in util.items(obj, exclude=exclude): File "/.venv/lib/python3.11/site-packages/jsonpickle/util.py", line 584, in items for k, v in obj.items(): RuntimeError: dictionary changed size during iteration |
We can see the dictionary of the fields changed the size because of the assignment. This would not be the case if we initialized the field in the constructor (i.e., if we didn’t need to add a new field to the object but to modify an existing one).