vega / altair

Declarative statistical visualization library for Python
https://altair-viz.github.io/
BSD 3-Clause "New" or "Revised" License
9.4k stars 795 forks source link

Make all features in Altair thread-safe #3589

Open binste opened 2 months ago

binste commented 2 months ago

What is your suggestion?

Right now (version 5.4.0), Altair is mostly safe to use in a threading context. However, we could go through the whole library to make sure everything is covered and then advertise this. Features which might require adjustments/locks:

Have you considered any alternative solutions?

This is not urgent and we can leave it as-is. Users and developers of other libraries can work around these limitations with locks. But it would be easier if we do it in Altair itself.

dangotbanned commented 2 months ago

If the stdlib considers this a thread safe counter, maybe that is an easy solution?

https://github.com/python/cpython/blob/6e06e01881dcffbeef5baac0c112ffb14cfa0b27/Lib/asyncio/tasks.py#L31-L34

# Helper to generate new task names
# This uses itertools.count() instead of a "+= 1" operation because the latter
# is not thread safe. See bpo-11866 for a longer explanation.
_task_name_counter = itertools.count(1).__next__

An example where I adapted this:

Code block ```py import itertools from collections.abc import Callable from typing import TYPE_CHECKING, Any, ClassVar, Protocol, runtime_checkable _counter_glob = itertools.count(1).__next__ @runtime_checkable class _AsyncNamed(Protocol): # Mixin providing uniquely named instances. _counter_cls: ClassVar[Callable[[], int]] _name: str def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None: super().__init_subclass__(*args, **kwargs) cls._counter_cls = itertools.count(1).__next__ def __init__(self) -> None: cls = type(self) self._name = f"{cls.__name__}-{cls._counter_cls()}-{_counter_glob()}" def _name_sub(self, obj: Any, /) -> str: other = obj.name if isinstance(obj, _AsyncNamed) else type(obj).__name__ return f"{self.name} > {other}" @property def name(self) -> str: return self._name ```