Repository: jupyter-server/pycrdt (language: python)

Issue:
Multithreaded Environment Support
### Problem
My team and I were hoping to use py-crdt in a server-side context with multiple workers. We ran into the issue of workers switching context and the rust binary panicking when resources were dropped by the original threads, complaining that the structs are "unsendable".

I skimmed through the source code and found the Rust definitions for the pycrdt structs that were causing these issues, and noticed that they were simple wrappers around yrs structs which do implement Send and Sync, but that the wrappers around them are explicitly marked as unsendable, which I thought might be related.

I did also see in related threads in other repositories that an async API system was being considered, and wanted to ask if there were any updates on that front, and if there was some other considerations I am unaware of that required structs to be unsendable? And if there was any proposed solution to this issue that is in the works?

Any information anyone could provide would be very helpful, thanks!



Discussion / hints:
Thank you for opening your first issue in this project! Engagement like this is essential for open source projects! :hugs:
<br>If you haven't done so already, check out [Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md). Also, please try to follow the issue template as it helps other other community members to contribute more effectively.
![welcome](https://raw.githubusercontent.com/jupyter-server/.github/master/images/welcome.jpg)
You can meet the other [Jovyans](https://jupyter.readthedocs.io/en/latest/community/content-community.html?highlight=jovyan#what-is-a-jovyan) by joining our [Discourse forum](http://discourse.jupyter.org/). There is also an intro thread there where you can stop by and say Hi! :wave:
<br>Welcome to the Jupyter community! :tada:

There is a [comment](https://github.com/jupyter-server/pycrdt/pull/6#issuecomment-1739860509) pointing to what should be done to allow multithreading, but I haven't gone any further.
I'm still interested though, and it would be great if you wanted to open a PR.
I opened #114.
Thank you for the prompt response! I took a look at the PR, and tried cloning + building but taking a closer look it looks as though some of the underlying yrs structs don't implement send and at some point we may need some Mutexing to make this thread-safe?

Performance issues aside, would wrapping each of the underlying yrs structs in the Python interface layer in an `Arc<Mutex<T>>` solve the issue? If so, is this something that would be helpful to implement and then provide some library interface that allows user to decide if they'd like to use a mutexed version of the library?
Good questions, I honestly don't know. Should we start by just having it work, using mutexes?
I think a Mutex implementation would be very nice to have, so long as it's kept as an optional configuration - applications that are single-threaded shouldn't incur the overhead of locking, etc.
Would this configuration consist of having two separate builds, one with mutexes and one without, and a runtime parameter for choosing one?
Note that even when not sending objects across threads, just creating an object in another thread is problematic since it can be garbage collected from a different thread, which will panic and lead to memory leaks. We can see that in `pytest` for instance, see [here](https://github.com/jupyter-server/pycrdt-websocket/actions/runs/9070072473/job/24921048294).
So it might be safer to always use mutexes.

Fix patch:
diff --git a/src/array.rs b/src/array.rs
index f648870..1a48c57 100644
--- a/src/array.rs
+++ b/src/array.rs
@@ -22,7 +22,7 @@ use crate::map::Map;
 use crate::doc::Doc;
 
 
-#[pyclass(unsendable)]
+#[pyclass]
 pub struct Array {
     pub array: ArrayRef,
 }
diff --git a/src/doc.rs b/src/doc.rs
index d020876..9c2871e 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -21,7 +21,7 @@ use crate::subscription::Subscription;
 use crate::type_conversions::ToPython;
 
 
-#[pyclass(unsendable)]
+#[pyclass]
 #[derive(Clone)]
 pub struct Doc {
     pub doc: _Doc,
diff --git a/src/map.rs b/src/map.rs
index 5681c13..79c28c5 100644
--- a/src/map.rs
+++ b/src/map.rs
@@ -22,7 +22,7 @@ use crate::array::Array;
 use crate::doc::Doc;
 
 
-#[pyclass(unsendable)]
+#[pyclass]
 pub struct Map {
     pub map: MapRef,
 }
diff --git a/src/text.rs b/src/text.rs
index e7ddea2..7970ca0 100644
--- a/src/text.rs
+++ b/src/text.rs
@@ -13,7 +13,7 @@ use crate::subscription::Subscription;
 use crate::type_conversions::ToPython;
 
 
-#[pyclass(unsendable)]
+#[pyclass]
 pub struct Text {
     pub text: TextRef,
 }


Test patch:
diff --git a/tests/test_threads.py b/tests/test_threads.py
new file mode 100644
index 0000000..a94a4d5
--- /dev/null
+++ b/tests/test_threads.py
@@ -0,0 +1,35 @@
+import gc
+from threading import Thread
+
+import pytest
+from pycrdt import Doc, Text
+
+
+def test_multi_threading():
+    doc = Doc()
+    doc["text"] = text = Text()
+    message = "Hello from thread!"
+
+    def add_text(text, message):
+        text += "Hello from thread!"
+
+    thread = Thread(target=add_text, args=(text, message))
+    thread.start()
+    thread.join()
+    assert str(text) == message
+
+    def drop():
+        nonlocal text, doc
+        del text
+        del doc
+        gc.collect()
+
+    thread = Thread(target=drop)
+    thread.start()
+    thread.join()
+
+    with pytest.raises(UnboundLocalError):
+        text
+
+    with pytest.raises(UnboundLocalError):
+        doc