44#include < cstddef>
55#include < exception>
66#include < mutex>
7+ #include < stdexcept>
78#include < thread>
89#include < utility>
910#include < vector>
@@ -29,17 +30,23 @@ inline std::size_t normalize_thread_count(
2930
3031// Split [0, number_of_work_items) into n_threads contiguous chunks and invoke
3132// `chunk(thread_id, begin, end)` on each chunk. Thread 0 runs on the calling
32- // thread; threads 1..n_threads-1 run in parallel std::threads . The caller is
33+ // thread; threads 1..n_threads-1 run in parallel std::jthreads . The caller is
3334// responsible for picking n_threads via normalize_thread_count and for the
34- // thread safety of `chunk`. If any chunk throws, every thread is still joined
35- // and the first exception is rethrown on the calling thread (the remaining
36- // chunks run to completion; there is no cancellation).
35+ // thread safety of `chunk`.
36+ //
37+ // Exceptions thrown by `chunk` are captured rather than allowed to escape a
38+ // worker thread (which would call std::terminate). The first exception on any
39+ // thread is stored and rethrown on the calling thread after every worker has
40+ // been joined, so nanobind can translate it into a Python exception.
3741template <class Chunk >
3842void parallel_for_chunks (
3943 const std::size_t n_threads,
4044 const std::size_t number_of_work_items,
4145 Chunk &&chunk
4246) {
47+ if (n_threads == 0 ) {
48+ throw std::invalid_argument (" parallel_for_chunks requires n_threads >= 1" );
49+ }
4350 const auto bounds = [&](const std::size_t thread_id) {
4451 const auto begin = thread_id * number_of_work_items / n_threads;
4552 const auto end = (thread_id + 1 ) * number_of_work_items / n_threads;
@@ -48,8 +55,10 @@ void parallel_for_chunks(
4855
4956 std::exception_ptr first_exception;
5057 std::mutex exception_mutex;
51- const auto guarded_chunk = [&](
52- const std::size_t thread_id, const std::size_t begin, const std::size_t end
58+ const auto guarded_chunk = [&] (
59+ const std::size_t thread_id,
60+ const std::size_t begin,
61+ const std::size_t end
5362 ) {
5463 try {
5564 chunk (thread_id, begin, end);
@@ -61,7 +70,11 @@ void parallel_for_chunks(
6170 }
6271 };
6372
64- std::vector<std::thread> threads;
73+ // jthread's destructor joins, so a failure while creating a later worker
74+ // cannot destroy an earlier still-joinable thread and terminate the
75+ // process. We still join explicitly below to rethrow only after all work
76+ // has completed.
77+ std::vector<std::jthread> threads;
6578 threads.reserve (n_threads > 0 ? n_threads - 1 : 0 );
6679 for (std::size_t thread_id = 1 ; thread_id < n_threads; ++thread_id) {
6780 const auto [begin, end] = bounds (thread_id);
0 commit comments