Skip to content

Commit aa5785c

Browse files
authored
Merge pull request #2 from K-T0BIAS/experimental
fix #1 and add a Barrier in cthreads.sync
2 parents 65ee752 + dd5da4a commit aa5785c

8 files changed

Lines changed: 100 additions & 7 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ __Threadable__/
5454
.cthreads_cache.json
5555
cthreads_kernels.dll
5656
cthreads_kernels.so
57+
cthreads_kernels.lib
5758
libcthreads_kernels.so
5859
libcthreads_kernels.dylib
5960
# <<< cthreads (auto)

src/cthreads/cpp/bindings/module.cpp

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "../headers/math/clamps.hpp"
1111
#include "../headers/math/random.hpp"
1212
#include "../headers/pyThread.hpp"
13+
#include "../headers/sync/pyBarrier.hpp"
1314
#include "../headers/sync/pyEvent.hpp"
1415
#include "../headers/sync/pyLock.hpp"
1516
#include "../headers/sync/pyRWLock.hpp"
@@ -675,16 +676,28 @@ std::shared_ptr<SpawnedKernel> spawn_from_meta(
675676
auto job = make_kernel_job(self, call_fn, params_keep);
676677
if (pool) {
677678
// Keep SpawnedKernel alive until the task runs or is dropped from the queue.
679+
// Python-owned handles in `job` / `spawned` must only be destroyed while the
680+
// GIL is held (pool workers are bare OS threads).
678681
struct PoolTaskState {
679682
std::shared_ptr<SpawnedKernel> spawned;
680683
std::function<void()> job;
681684
std::atomic<bool> started{false};
685+
686+
void release_python_state() {
687+
py::gil_scoped_acquire gil;
688+
job = nullptr;
689+
spawned.reset();
690+
}
691+
682692
~PoolTaskState() {
693+
py::gil_scoped_acquire gil;
683694
if (!started.load(std::memory_order_acquire) && spawned) {
684695
spawned->mark_done(std::make_exception_ptr(std::runtime_error(
685696
"cthreads.pool: job dropped (pool stopped before run)"
686697
)));
687698
}
699+
job = nullptr;
700+
spawned.reset();
688701
}
689702
};
690703
auto st = std::make_shared<PoolTaskState>();
@@ -699,6 +712,7 @@ std::shared_ptr<SpawnedKernel> spawn_from_meta(
699712
} catch (...) {
700713
st->spawned->mark_done(std::current_exception());
701714
}
715+
st->release_python_state();
702716
});
703717
} else {
704718
// Dedicated path (unchanged): one OS thread via CThread.
@@ -837,8 +851,11 @@ std::uintptr_t sync_native_ptr(py::object obj) {
837851
if (py::isinstance<cthreads::sync::RWLock>(obj)) {
838852
return reinterpret_cast<std::uintptr_t>(&obj.cast<cthreads::sync::RWLock&>());
839853
}
854+
if (py::isinstance<cthreads::sync::Barrier>(obj)) {
855+
return reinterpret_cast<std::uintptr_t>(&obj.cast<cthreads::sync::Barrier&>());
856+
}
840857
throw std::runtime_error(
841-
"cthreads.sync_native_ptr: expected Lock, Event, or RWLock");
858+
"cthreads.sync_native_ptr: expected Lock, Event, RWLock, or Barrier");
842859
}
843860

844861
#include "pool.tpp"
@@ -877,7 +894,7 @@ PYBIND11_MODULE(_ext, m) {
877894
"sync_native_ptr",
878895
&sync_native_ptr,
879896
py::arg("obj"),
880-
"Return the native address of a cthreads.sync Lock/Event/RWLock "
897+
"Return the native address of a cthreads.sync Lock/Event/RWLock/Barrier "
881898
"for kernel marshalling."
882899
);
883900

@@ -995,6 +1012,14 @@ PYBIND11_MODULE(_ext, m) {
9951012

9961013
rwlock.attr("__cthreads_internal__") = true;
9971014

1015+
auto barrier = py::class_<cthreads::sync::Barrier>(sync, "Barrier")
1016+
.def(py::init<std::size_t>(), py::arg("parties"))
1017+
.def("parties", &cthreads::sync::Barrier::parties)
1018+
.def("arrive_and_wait", &cthreads::sync::Barrier::arrive_and_wait,
1019+
py::call_guard<py::gil_scoped_release>());
1020+
1021+
barrier.attr("__cthreads_internal__") = true;
1022+
9981023
// Fixed-capacity triple buffers (baseline bindings for primitive/container types).
9991024
// Thread-side writes are typically codegen'd as: buf[i].field = ... (for Threadables)
10001025
// or buf[i] = value (for scalar/container slots).

src/cthreads/cpp/headers/pool/threadPool.hpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,15 @@ namespace cthreads::pool {
6969
this->cv.notify_all(); // notify all waiting worker threads that the pool is stopping
7070
this->join(); // wait for in-flight tasks; queued tasks are dropped below
7171
this->threads.clear();
72+
// Destroy dropped tasks outside the queue mutex: their destructors may
73+
// acquire the GIL, and Python code can wait on this mutex while holding it.
74+
std::queue<std::function<void()>> dropped;
7275
{
7376
std::lock_guard<std::mutex> lock(this->tasks_queue_mutex);
74-
while (!this->tasks_queue.empty()) {
75-
this->tasks_queue.pop();
76-
}
77+
dropped.swap(this->tasks_queue);
78+
}
79+
while (!dropped.empty()) {
80+
dropped.pop();
7781
}
7882
this->started = false;
7983
this->stop_signal.store(false);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) 2026 Tobias Karusseit
2+
// This source code is licensed under the MIT license found in the
3+
// LICENSE file in the root directory of this source tree.
4+
5+
#pragma once
6+
7+
#include <condition_variable>
8+
#include <cstddef>
9+
#include <mutex>
10+
#include <stdexcept>
11+
12+
namespace cthreads::sync {
13+
14+
/**
15+
* Fixed-party generation barrier for long-lived @Thread workers.
16+
*
17+
* All `parties` threads must call `arrive_and_wait()` before any proceeds.
18+
* Reusable across phases (grid → density → dynamics → …).
19+
*/
20+
class Barrier {
21+
std::mutex _mu;
22+
std::condition_variable _cv;
23+
const std::size_t _parties;
24+
std::size_t _count = 0;
25+
std::size_t _generation = 0;
26+
27+
public:
28+
explicit Barrier(std::size_t parties) : _parties(parties) {
29+
if (parties == 0) {
30+
throw std::invalid_argument("cthreads.sync.Barrier: parties must be >= 1");
31+
}
32+
}
33+
34+
Barrier(const Barrier&) = delete;
35+
Barrier& operator=(const Barrier&) = delete;
36+
37+
std::size_t parties() const { return _parties; }
38+
39+
void arrive_and_wait() {
40+
std::unique_lock<std::mutex> g(_mu);
41+
const std::size_t gen = _generation;
42+
if (++_count == _parties) {
43+
_count = 0;
44+
++_generation;
45+
_cv.notify_all();
46+
return;
47+
}
48+
_cv.wait(g, [this, gen] { return gen != _generation; });
49+
}
50+
};
51+
52+
} // namespace cthreads::sync

src/cthreads/python/cthreads/compiler/translation/plugins/sync/Sync.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ class SyncMethodPlugin(MethodTablePlugin):
4545
"release_write": method_op("release_write", 0),
4646
"try_acquire_write": method_op("try_acquire_write", 0),
4747
},
48+
"Barrier": {
49+
"arrive_and_wait": method_op("arrive_and_wait", 0),
50+
"parties": method_op("parties", 0),
51+
},
4852
}
4953

5054
def type_key(self, py_type: PyType) -> str | None:

src/cthreads/python/cthreads/sync/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@
2929
Lock = _native.Lock
3030
Event = _native.Event
3131
RWLock = getattr(_native, "RWLock", None)
32+
Barrier = getattr(_native, "Barrier", None)
3233
TBufferI64 = getattr(_native, "TBufferI64", None)
3334
else:
3435
Lock = None # type: ignore[assignment,misc]
3536
Event = None # type: ignore[assignment,misc]
3637
RWLock = None # type: ignore[assignment,misc]
38+
Barrier = None # type: ignore[assignment,misc]
3739
TBufferI64 = None # type: ignore[assignment,misc]
3840

3941
__all__ = [
@@ -48,5 +50,6 @@
4850
"Lock",
4951
"Event",
5052
"RWLock",
53+
"Barrier",
5154
"TBufferI64",
5255
]

src/cthreads/python/cthreads/types/pyType/internal/TBuffer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def is_tbuffer_pytype(py_type: PyType) -> bool:
6464

6565

6666
def is_sync_pytype(py_type: PyType) -> bool:
67-
"""True for `Lock` / `Event` / `RWLock` kernel params (non-copyable)."""
67+
"""True for `Lock` / `Event` / `RWLock` / `Barrier` kernel params (non-copyable)."""
6868
return (
6969
isinstance(py_type, PyCThreadsInternalType)
7070
and py_type.name in SYNC_INTERNAL_NAMES

src/cthreads/python/cthreads/types/pyType/internal/include_map.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Native types shipped in the pybind module (not codegen'd as Threadables).
22
# keyed by Python class __name__ as exposed on cthreads.sync / cthreads.linalg
3-
SYNC_INTERNAL_NAMES: frozenset[str] = frozenset({"Lock", "Event", "RWLock"})
3+
SYNC_INTERNAL_NAMES: frozenset[str] = frozenset({"Lock", "Event", "RWLock", "Barrier"})
44

55
CTHREADS_INTERNAL_TYPES: dict[str, dict[str, str]] = {
66
"Lock": {
@@ -15,6 +15,10 @@
1515
"cpp_name": "cthreads::sync::RWLock",
1616
"cpp_include": "sync/pyRWLock.hpp",
1717
},
18+
"Barrier": {
19+
"cpp_name": "cthreads::sync::Barrier",
20+
"cpp_include": "sync/pyBarrier.hpp",
21+
},
1822
# Fixed-capacity triple buffers (cthreads.sync.TBuffer*).
1923
"TBufferF64": {
2024
"cpp_name": "cthreads::sync::tripple_buffer<double>",

0 commit comments

Comments
 (0)